@statewalker/webrun-http-browser 0.6.5 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +302 -52
  2. package/dist/core/data-calls.d.ts +17 -0
  3. package/dist/core/data-calls.d.ts.map +1 -1
  4. package/dist/core/service-worker-control.d.ts +3 -1
  5. package/dist/core/service-worker-control.d.ts.map +1 -1
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4218 -301
  9. package/dist/link/async-dispose.d.ts +10 -0
  10. package/dist/link/async-dispose.d.ts.map +1 -0
  11. package/dist/link/errors.d.ts +2 -0
  12. package/dist/link/errors.d.ts.map +1 -0
  13. package/dist/link/http-link.d.ts +72 -0
  14. package/dist/link/http-link.d.ts.map +1 -0
  15. package/dist/link/index.d.ts +4 -0
  16. package/dist/link/index.d.ts.map +1 -0
  17. package/dist/link/locks.d.ts +39 -0
  18. package/dist/link/locks.d.ts.map +1 -0
  19. package/dist/link/rendezvous.d.ts +21 -0
  20. package/dist/link/rendezvous.d.ts.map +1 -0
  21. package/dist/relay/index-sw.d.ts +12 -2
  22. package/dist/relay/index-sw.d.ts.map +1 -1
  23. package/dist/relay/index.d.ts +66 -8
  24. package/dist/relay/index.d.ts.map +1 -1
  25. package/dist/relay-sw.js +1328 -64
  26. package/dist/relay-worker.js +1329 -64
  27. package/dist/sw/handler-endpoint.d.ts +81 -0
  28. package/dist/sw/handler-endpoint.d.ts.map +1 -0
  29. package/dist/sw/handler-table.d.ts +53 -0
  30. package/dist/sw/handler-table.d.ts.map +1 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +162 -21
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -1
  33. package/dist/sw/index.d.ts +5 -0
  34. package/dist/sw/index.d.ts.map +1 -1
  35. package/dist/sw/protocol.d.ts +115 -0
  36. package/dist/sw/protocol.d.ts.map +1 -0
  37. package/dist/sw/sw-dispatcher.d.ts.map +1 -1
  38. package/dist/sw/sw-http-adapter.d.ts +199 -0
  39. package/dist/sw/sw-http-adapter.d.ts.map +1 -0
  40. package/dist/sw/sw-websocket.d.ts +16 -0
  41. package/dist/sw/sw-websocket.d.ts.map +1 -0
  42. package/dist/sw/worker-service.d.ts +45 -0
  43. package/dist/sw/worker-service.d.ts.map +1 -0
  44. package/dist/sw/worker-url.d.ts +9 -0
  45. package/dist/sw/worker-url.d.ts.map +1 -0
  46. package/dist/sw-worker.js +2403 -265
  47. package/dist/sw.d.ts +2 -0
  48. package/dist/sw.d.ts.map +1 -1
  49. package/dist/sw.js +4189 -546
  50. package/dist/worker.d.ts +10 -0
  51. package/dist/worker.d.ts.map +1 -0
  52. package/dist/worker.js +3780 -0
  53. package/dist/ws/client.d.ts +73 -0
  54. package/dist/ws/client.d.ts.map +1 -0
  55. package/dist/ws/frames.d.ts +68 -0
  56. package/dist/ws/frames.d.ts.map +1 -0
  57. package/dist/ws/index.d.ts +6 -0
  58. package/dist/ws/index.d.ts.map +1 -0
  59. package/dist/ws/server.d.ts +53 -0
  60. package/dist/ws/server.d.ts.map +1 -0
  61. package/dist/ws/socket-core.d.ts +91 -0
  62. package/dist/ws/socket-core.d.ts.map +1 -0
  63. package/dist/ws/upgrade.d.ts +93 -0
  64. package/dist/ws/upgrade.d.ts.map +1 -0
  65. package/package.json +10 -5
  66. package/src/core/data-calls.ts +52 -2
  67. package/src/core/service-worker-control.ts +7 -1
  68. package/src/index.ts +3 -0
  69. package/src/link/async-dispose.ts +22 -0
  70. package/src/link/errors.ts +3 -0
  71. package/src/link/http-link.ts +238 -0
  72. package/src/link/index.ts +3 -0
  73. package/src/link/locks.ts +97 -0
  74. package/src/link/rendezvous.ts +60 -0
  75. package/src/relay/index-sw.ts +84 -12
  76. package/src/relay/index.ts +203 -27
  77. package/src/sw/handler-endpoint.ts +278 -0
  78. package/src/sw/handler-table.ts +130 -0
  79. package/src/sw/http-sw-dispatcher.ts +649 -90
  80. package/src/sw/index.ts +5 -0
  81. package/src/sw/protocol.ts +103 -0
  82. package/src/sw/sw-dispatcher.ts +1 -20
  83. package/src/sw/sw-http-adapter.ts +613 -0
  84. package/src/sw/sw-websocket.ts +43 -0
  85. package/src/sw/worker-service.ts +133 -0
  86. package/src/sw/worker-url.ts +19 -0
  87. package/src/sw-worker.ts +8 -3
  88. package/src/sw.ts +2 -0
  89. package/src/worker.ts +9 -0
  90. package/src/ws/client.ts +346 -0
  91. package/src/ws/frames.ts +119 -0
  92. package/src/ws/index.ts +11 -0
  93. package/src/ws/server.ts +132 -0
  94. package/src/ws/socket-core.ts +251 -0
  95. package/src/ws/upgrade.ts +200 -0
@@ -0,0 +1,73 @@
1
+ import { type WsTimers } from "./frames.js";
2
+ /** What `connect` gives a socket once the server accepted it (the 101). */
3
+ export interface WebSocketConnection {
4
+ port: MessagePort;
5
+ /** The subprotocol the server picked, `""` for none. */
6
+ protocol: string;
7
+ }
8
+ /**
9
+ * Opens the transport of one socket: resolves once the server accepted the
10
+ * upgrade, rejects when it refused or could not be reached. Where the port
11
+ * comes from — a ServiceWorker, a relay, a handler in the same page — is the
12
+ * caller's business.
13
+ */
14
+ export type WebSocketConnector = (url: string, protocols: string[]) => Promise<WebSocketConnection>;
15
+ export interface WebSocketClassOptions {
16
+ /** Ping period, in ms. Pongs missing for twice that close the socket with 1006. Default 10 s. */
17
+ heartbeatMs?: number;
18
+ /** Timer functions; see `WsTimers`. */
19
+ timers?: WsTimers;
20
+ /** How long `close()` waits for the server's echo, in ms. Default 5 s. */
21
+ closeTimeoutMs?: number;
22
+ }
23
+ type Listener = ((event: Event) => void) | null;
24
+ /** The instance side of the class `createWebSocketClass` returns: the WHATWG `WebSocket` surface. */
25
+ export interface PortWebSocket extends EventTarget {
26
+ readonly url: string;
27
+ readonly protocol: string;
28
+ readonly extensions: string;
29
+ readonly readyState: number;
30
+ readonly bufferedAmount: number;
31
+ binaryType: BinaryType;
32
+ onopen: Listener;
33
+ onmessage: ((event: MessageEvent) => void) | null;
34
+ onerror: Listener;
35
+ onclose: ((event: CloseEvent) => void) | null;
36
+ send(data: string | ArrayBufferLike | ArrayBufferView | Blob): void;
37
+ close(code?: number, reason?: string): void;
38
+ /** Not in the standard: see `PortSocketCore.whenDrained`. */
39
+ whenDrained(limit?: number): Promise<void>;
40
+ readonly CONNECTING: 0;
41
+ readonly OPEN: 1;
42
+ readonly CLOSING: 2;
43
+ readonly CLOSED: 3;
44
+ }
45
+ export interface PortWebSocketClass {
46
+ new (url: string | URL, protocols?: string | string[]): PortWebSocket;
47
+ readonly CONNECTING: 0;
48
+ readonly OPEN: 1;
49
+ readonly CLOSING: 2;
50
+ readonly CLOSED: 3;
51
+ }
52
+ /**
53
+ * A WHATWG-compatible `WebSocket` class whose transport is a `MessagePort`
54
+ * from `connect`. Code written against the browser's `WebSocket` runs on it
55
+ * unchanged:
56
+ *
57
+ * ```ts
58
+ * const WS = createWebSocketClass(adapter.connectWebSocket);
59
+ * const ws = new WS(`${baseUrl}chat`, ["v1"]);
60
+ * ws.onmessage = (event) => console.log(event.data);
61
+ * ```
62
+ *
63
+ * It validates as the standard does (url, scheme, fragment, subprotocol
64
+ * tokens and duplicates, close codes and reasons), has `binaryType`, the `on*`
65
+ * handlers next to `addEventListener`, and the close handshake.
66
+ * `bufferedAmount` is real: the server acknowledges what it consumed.
67
+ *
68
+ * The real `globalThis.WebSocket` is left alone; install the class where you
69
+ * want it.
70
+ */
71
+ export declare function createWebSocketClass(connect: WebSocketConnector, options?: WebSocketClassOptions): PortWebSocketClass;
72
+ export {};
73
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/ws/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAa9E,2EAA2E;AAC3E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,WAAW,CAAC;IAClB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAEpG,MAAM,WAAW,qBAAqB;IACpC,iGAAiG;IACjG,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,QAAQ,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;AAEhD,qGAAqG;AACrG,MAAM,WAAW,aAAc,SAAQ,WAAW;IAChD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,QAAQ,CAAC;IACjB,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,OAAO,EAAE,QAAQ,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;IACpE,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,6DAA6D;IAC7D,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,CAAC;IACtE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;CACpB;AAKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,GAAE,qBAA0B,GAClC,kBAAkB,CAsNpB"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The wire protocol of a WebSocket carried over a `MessagePort`.
3
+ *
4
+ * A `MessagePort` is a natural carrier for a WebSocket: messages arrive in
5
+ * order, and strings and transferable `ArrayBuffer`s cross it as they are. The
6
+ * protocol on top is these frames, the same in both directions:
7
+ *
8
+ * - `m` — one message. An `ArrayBuffer` is transferred, never copied.
9
+ * - `c` — close. The side that closes first sends it; the other answers with
10
+ * its own `c` (the echo), and both are then closed cleanly.
11
+ * - `p` — ping, client to server, carrying the client's heartbeat period.
12
+ * - `q` — pong, server to client.
13
+ * - `a` — ack: the receiver consumed `n` more bytes of messages. This is the
14
+ * flow control: what a side has sent and not yet seen acknowledged is its
15
+ * `bufferedAmount`, and the server end can wait for it to fall
16
+ * (`whenDrained`). Added in version 2; version 1 had no backpressure.
17
+ *
18
+ * HEARTBEAT. A `MessagePort` never reports that the other side died, so the
19
+ * client pings every `hb` ms and closes with 1006 when pongs stop for `2·hb`;
20
+ * the server closes with 1006 when pings stop for `3·hb`. That is how a socket
21
+ * notices a closed tab or a terminated worker.
22
+ *
23
+ * Unknown frames are ignored, so a later version can add frames a peer of
24
+ * this version skips.
25
+ */
26
+ /** The version of this frame protocol. Sent in the upgrade hand-shake. */
27
+ export declare const WS_PORT_PROTOCOL_VERSION = 2;
28
+ /** Default heartbeat period of the client, in ms. */
29
+ export declare const DEFAULT_HEARTBEAT_MS = 10000;
30
+ /** How long a side waits for the echo of its close frame, in ms. */
31
+ export declare const DEFAULT_CLOSE_TIMEOUT_MS = 5000;
32
+ export type WsFrame = {
33
+ t: "m";
34
+ d: string | ArrayBuffer;
35
+ } | {
36
+ t: "c";
37
+ code: number;
38
+ reason: string;
39
+ } | {
40
+ t: "p";
41
+ hb: number;
42
+ } | {
43
+ t: "q";
44
+ } | {
45
+ t: "a";
46
+ n: number;
47
+ };
48
+ /**
49
+ * The timer functions a socket uses. On a page's main thread, background
50
+ * throttling delays timers by up to a minute, which would close healthy
51
+ * sockets; pass unthrottled ones (worker-timers, say) there.
52
+ */
53
+ export interface WsTimers {
54
+ setTimeout(fn: () => void, ms: number): unknown;
55
+ clearTimeout(id: unknown): void;
56
+ setInterval(fn: () => void, ms: number): unknown;
57
+ clearInterval(id: unknown): void;
58
+ }
59
+ export declare const defaultTimers: WsTimers;
60
+ /** Recovers a frame from a message's data, or `undefined` for anything else. */
61
+ export declare function readFrame(data: unknown): WsFrame | undefined;
62
+ /** Posts `frame`, transferring a message's `ArrayBuffer`. */
63
+ export declare function postFrame(port: MessagePort, frame: WsFrame): void;
64
+ /** The size a message counts for in `bufferedAmount`: UTF-8 bytes, or the buffer's length. */
65
+ export declare function messageSize(data: string | ArrayBuffer): number;
66
+ /** The UTF-8 length of `text`, without encoding it. */
67
+ export declare function utf8Length(text: string): number;
68
+ //# sourceMappingURL=frames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frames.d.ts","sourceRoot":"","sources":["../../src/ws/frames.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,0EAA0E;AAC1E,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAE1C,qDAAqD;AACrD,eAAO,MAAM,oBAAoB,QAAS,CAAC;AAE3C,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AAE9C,MAAM,MAAM,OAAO,GACf;IAAE,CAAC,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,GACnC;IAAE,CAAC,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACxC;IAAE,CAAC,EAAE,GAAG,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACtB;IAAE,CAAC,EAAE,GAAG,CAAA;CAAE,GACV;IAAE,CAAC,EAAE,GAAG,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1B;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,YAAY,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IACjD,aAAa,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAC;CAClC;AAED,eAAO,MAAM,aAAa,EAAE,QAK3B,CAAC;AAEF,gFAAgF;AAChF,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,SAAS,CA2B5D;AAED,6DAA6D;AAC7D,wBAAgB,SAAS,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAGjE;AAED,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,MAAM,CAE9D;AAED,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAe/C"}
@@ -0,0 +1,6 @@
1
+ export * from "./client.js";
2
+ export { DEFAULT_CLOSE_TIMEOUT_MS, DEFAULT_HEARTBEAT_MS, WS_PORT_PROTOCOL_VERSION, type WsFrame, type WsTimers, } from "./frames.js";
3
+ export * from "./server.js";
4
+ export { WsCloseEvent, type WsCloseEventInit } from "./socket-core.js";
5
+ export * from "./upgrade.js";
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ws/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EACL,wBAAwB,EACxB,oBAAoB,EACpB,wBAAwB,EACxB,KAAK,OAAO,EACZ,KAAK,QAAQ,GACd,MAAM,aAAa,CAAC;AACrB,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,YAAY,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACvE,cAAc,cAAc,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { type WsFrame, type WsTimers } from "./frames.js";
2
+ import { PortSocketCore } from "./socket-core.js";
3
+ export interface ServerWebSocketOptions {
4
+ /** The url the client asked for. */
5
+ url?: string;
6
+ /** The subprotocol the server picked, `""` for none. */
7
+ protocol?: string;
8
+ timers?: WsTimers;
9
+ /** How long `close()` waits for the client's echo, in ms. Default 5 s. */
10
+ closeTimeoutMs?: number;
11
+ }
12
+ /**
13
+ * The server end of a socket over a `MessagePort`: what a handler gets from
14
+ * `upgradeWebSocket` and talks to.
15
+ *
16
+ * - Events are delivered only after `accept()`; frames that arrive earlier are
17
+ * queued. Pings are answered even before `accept()`, so a slow handler does
18
+ * not look like a dead one.
19
+ * - Messages arrive as strings or `ArrayBuffer`s.
20
+ * - `bufferedAmount` counts what the client has not consumed yet, and
21
+ * `whenDrained(limit)` waits for it to fall: the backpressure a producer
22
+ * applies.
23
+ * - When the client's pings stop for three heartbeat periods (a closed tab, a
24
+ * terminated worker), the socket closes with 1006.
25
+ */
26
+ export declare class ServerWebSocket extends PortSocketCore {
27
+ static readonly CONNECTING = 0;
28
+ static readonly OPEN = 1;
29
+ static readonly CLOSING = 2;
30
+ static readonly CLOSED = 3;
31
+ readonly url: string;
32
+ readonly protocol: string;
33
+ private _accepted;
34
+ private _queue;
35
+ private _heartbeatMs;
36
+ private _lastPing;
37
+ private _watch;
38
+ constructor(port: MessagePort, options?: ServerWebSocketOptions);
39
+ /** Whether `accept()` was called. */
40
+ get accepted(): boolean;
41
+ /** Starts delivering events, the queued ones first. */
42
+ accept(): void;
43
+ send(data: string | ArrayBuffer | ArrayBufferView): void;
44
+ close(code?: number, reason?: string): void;
45
+ protected _deliver(frame: Extract<WsFrame, {
46
+ t: "m" | "c";
47
+ }>): void;
48
+ protected _dispatchMessage(data: string | ArrayBuffer): void;
49
+ protected _onPing(hb: number): void;
50
+ protected _onClosed(): void;
51
+ private _watchHeartbeat;
52
+ }
53
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/ws/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5F,OAAO,EAML,cAAc,EAEf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,0EAA0E;IAC1E,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;;;;;;;;GAaG;AACH,qBAAa,eAAgB,SAAQ,cAAc;IACjD,MAAM,CAAC,QAAQ,CAAC,UAAU,KAAc;IACxC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAQ;IAC5B,MAAM,CAAC,QAAQ,CAAC,OAAO,KAAW;IAClC,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAU;IAEhC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAiD;IAC/D,OAAO,CAAC,YAAY,CAAwB;IAC5C,OAAO,CAAC,SAAS,CAAc;IAC/B,OAAO,CAAC,MAAM,CAAU;IAExB,YAAY,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,sBAA2B,EAOlE;IAED,qCAAqC;IACrC,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,uDAAuD;IACvD,MAAM,IAAI,IAAI,CAIb;IAED,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,eAAe,GAAG,IAAI,CAKvD;IAED,KAAK,CAAC,IAAI,SAAO,EAAE,MAAM,SAAK,GAAG,IAAI,CAWpC;IAED,UAAmB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE;QAAE,CAAC,EAAE,GAAG,GAAG,GAAG,CAAA;KAAE,CAAC,GAAG,IAAI,CAM3E;IAED,SAAS,CAAC,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAE3D;IAED,UAAmB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAO3C;IAED,UAAmB,SAAS,IAAI,IAAI,CAGnC;IAED,OAAO,CAAC,eAAe;CAQxB"}
@@ -0,0 +1,91 @@
1
+ import { type WsFrame, type WsTimers } from "./frames.js";
2
+ export declare const CONNECTING = 0;
3
+ export declare const OPEN = 1;
4
+ export declare const CLOSING = 2;
5
+ export declare const CLOSED = 3;
6
+ export interface WsCloseEventInit extends EventInit {
7
+ code?: number;
8
+ reason?: string;
9
+ wasClean?: boolean;
10
+ }
11
+ /**
12
+ * A `CloseEvent` for runtimes that have none: Node 22 has `Event` and
13
+ * `MessageEvent` as globals, but not `CloseEvent`.
14
+ */
15
+ export declare class WsCloseEvent extends Event {
16
+ readonly code: number;
17
+ readonly reason: string;
18
+ readonly wasClean: boolean;
19
+ constructor(type: string, init?: WsCloseEventInit);
20
+ }
21
+ /** A `close` event: the platform's `CloseEvent` where there is one. */
22
+ export declare function newCloseEvent(init: WsCloseEventInit): Event;
23
+ export interface PortSocketOptions {
24
+ timers?: WsTimers;
25
+ /** How long to wait for the echo of a close frame before giving up with 1006. */
26
+ closeTimeoutMs?: number;
27
+ }
28
+ /**
29
+ * What both ends of a socket over a `MessagePort` share: the frames, the close
30
+ * handshake, and the byte accounting behind `bufferedAmount`.
31
+ *
32
+ * Not exported from the package: the two ends are `createWebSocketClass`'s
33
+ * class and `ServerWebSocket`.
34
+ */
35
+ export declare abstract class PortSocketCore extends EventTarget {
36
+ protected _port?: MessagePort;
37
+ protected _state: number;
38
+ protected readonly _timers: WsTimers;
39
+ protected readonly _closeTimeoutMs: number;
40
+ /** Bytes sent and not yet acknowledged by the peer, plus those still being prepared. */
41
+ private _inFlight;
42
+ /** Bytes of messages "sent" after the socket started closing: counted, never sent. */
43
+ private _discarded;
44
+ private _toAck;
45
+ private _ackScheduled;
46
+ private _closeTimer;
47
+ private _drainWaiters;
48
+ constructor(options?: PortSocketOptions);
49
+ get readyState(): number;
50
+ /**
51
+ * Bytes queued by `send()` that the peer has not consumed yet. A peer
52
+ * acknowledges what it has dispatched, so this falls as the other side reads.
53
+ */
54
+ get bufferedAmount(): number;
55
+ /**
56
+ * Resolves once `bufferedAmount` is at most `limit` (default 0), or the
57
+ * socket is closed. The backpressure a producer applies:
58
+ * `if (socket.bufferedAmount > HIGH) await socket.whenDrained(LOW)`.
59
+ */
60
+ whenDrained(limit?: number): Promise<void>;
61
+ protected _attach(port: MessagePort): void;
62
+ protected _onFrame(frame: WsFrame): void;
63
+ /** A message or close frame the socket is ready to act on. */
64
+ protected _deliver(frame: Extract<WsFrame, {
65
+ t: "m" | "c";
66
+ }>): void;
67
+ protected abstract _dispatchMessage(data: string | ArrayBuffer): void;
68
+ protected _onPing(_hb: number): void;
69
+ protected _onPong(): void;
70
+ protected _onClosed(): void;
71
+ /** Acknowledge consumed bytes, coalescing the acks of one turn into one frame. */
72
+ protected _consumed(bytes: number): void;
73
+ /** Reserves `bytes` in `bufferedAmount` for a message still being prepared (a Blob). */
74
+ protected _reserve(bytes: number): void;
75
+ /** Sends one message whose bytes were reserved with `_reserve` (or `reserved = false`). */
76
+ protected _sendData(data: string | ArrayBuffer, reserved?: boolean): void;
77
+ protected _post(frame: WsFrame): void;
78
+ /** Starts the close handshake. */
79
+ protected _startClose(code: number, reason: string): void;
80
+ protected _onCloseFrame(code: number, reason: string): void;
81
+ /** The socket is closed: release the port and tell the listeners. */
82
+ protected _finish(code: number, reason: string, wasClean: boolean, error?: boolean): void;
83
+ private _wakeDrainWaiters;
84
+ }
85
+ /** Copies `data` into a fresh `ArrayBuffer`: `send()` must never detach the caller's buffer. */
86
+ export declare function toArrayBuffer(data: ArrayBuffer | ArrayBufferView): ArrayBuffer;
87
+ export declare function isBinary(data: unknown): data is ArrayBuffer | ArrayBufferView;
88
+ export declare function isBlob(data: unknown): data is Blob;
89
+ /** Valid close codes for an application to send: 1000, or 3000–4999. */
90
+ export declare function isApplicationCloseCode(code: number): boolean;
91
+ //# sourceMappingURL=socket-core.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"socket-core.d.ts","sourceRoot":"","sources":["../../src/ws/socket-core.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,93 @@
1
+ import { ServerWebSocket, type ServerWebSocketOptions } from "./server.js";
2
+ /**
3
+ * The upgrade, as an HTTP exchange: a `GET` with `Upgrade: websocket` goes to
4
+ * an ordinary `(Request) => Response` handler, and a `101` answer carrying a
5
+ * server socket means accepted. Anything else is a refusal, with its status.
6
+ *
7
+ * FORBIDDEN HEADERS. A browser drops `Upgrade`, `Connection` and every
8
+ * `Sec-*` header from a `new Request()`. The upgrade request therefore carries
9
+ * its headers as an own `headers` property, a standalone `Headers` that keeps
10
+ * them, so handler code reads `request.headers.get("upgrade")` as usual.
11
+ *
12
+ * NO 101 IN A `Response`. The constructor refuses statuses below 200, so the
13
+ * response `upgradeWebSocket` returns reports 101 through an own `status`
14
+ * property; the port that goes back to the caller is found with `takePort`.
15
+ */
16
+ /** The request header carrying the client's version of the frame protocol. */
17
+ export declare const WS_VERSION_HEADER = "x-webrun-ws-version";
18
+ export interface WebSocketUpgradeInit {
19
+ /** Requested subprotocols, in order of preference. */
20
+ protocols?: string[];
21
+ /** More headers for the handler to see (cookies, origin, authorization...). */
22
+ headers?: HeadersInit;
23
+ }
24
+ /**
25
+ * The `GET` a handler sees for `new WebSocket(url, protocols)`. A `ws:` or
26
+ * `wss:` url is given as `http:` or `https:`, as a server sees it.
27
+ */
28
+ export declare function newWebSocketUpgradeRequest(url: string | URL, { protocols, headers }?: WebSocketUpgradeInit): Request;
29
+ /**
30
+ * Re-attaches `headers` to `request` as its own `headers` property, keeping
31
+ * the forbidden ones a browser dropped. Returns `request`.
32
+ */
33
+ export declare function withHeaders(request: Request, headers: Headers): Request;
34
+ /** Whether `request` asks for a WebSocket. */
35
+ export declare function isWebSocketUpgrade(request: Request): boolean;
36
+ /** The subprotocols `request` asks for, in order of preference. */
37
+ export declare function requestedProtocols(request: Request): string[];
38
+ export interface UpgradeWebSocketOptions extends Omit<ServerWebSocketOptions, "url"> {
39
+ }
40
+ export interface WebSocketUpgrade {
41
+ /** The server end. Call `accept()`, then listen and send. */
42
+ socket: ServerWebSocket;
43
+ /** Return it from the handler: status 101. */
44
+ response: Response;
45
+ }
46
+ interface PendingUpgrade {
47
+ port: MessagePort;
48
+ protocol: string;
49
+ }
50
+ /**
51
+ * Accepts a WebSocket upgrade in a handler:
52
+ *
53
+ * ```ts
54
+ * if (isWebSocketUpgrade(request)) {
55
+ * const { socket, response } = upgradeWebSocket(request, { protocol: "v1" });
56
+ * socket.accept();
57
+ * socket.addEventListener("message", (event) => socket.send(event.data));
58
+ * return response;
59
+ * }
60
+ * ```
61
+ *
62
+ * The host takes the client's port from the response with `takePort` and
63
+ * hands it to the caller.
64
+ */
65
+ export declare function upgradeWebSocket(request: Request, options?: UpgradeWebSocketOptions): WebSocketUpgrade;
66
+ /**
67
+ * The port to hand to the caller, for a response from `upgradeWebSocket`.
68
+ * Taken once: a second call returns `undefined`.
69
+ */
70
+ export declare function takePort(response: Response): MessagePort | undefined;
71
+ /** `takePort`, with the subprotocol the server picked. */
72
+ export declare function takeUpgrade(response: Response): PendingUpgrade | undefined;
73
+ export type WebSocketUpgradeResult = {
74
+ ok: true;
75
+ port: MessagePort;
76
+ protocol: string;
77
+ } | {
78
+ ok: false;
79
+ status: number;
80
+ message: string;
81
+ };
82
+ export interface ServeWebSocketUpgradeInit extends WebSocketUpgradeInit {
83
+ url: string;
84
+ }
85
+ /**
86
+ * Runs an upgrade through `handler`, as a host does when a caller asks for a
87
+ * socket: builds the upgrade request, calls the handler, and either takes the
88
+ * port of a `101` or reports the refusal. A subprotocol the caller did not ask
89
+ * for is a refusal, as in the standard.
90
+ */
91
+ export declare function serveWebSocketUpgrade(handler: (request: Request) => Response | Promise<Response>, { url, protocols, headers }: ServeWebSocketUpgradeInit): Promise<WebSocketUpgradeResult>;
92
+ export {};
93
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/ws/upgrade.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE3E;;;;;;;;;;;;;GAaG;AAEH,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,wBAAwB,CAAC;AAEvD,MAAM,WAAW,oBAAoB;IACnC,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,EAAE,SAAc,EAAE,OAAO,EAAE,GAAE,oBAAyB,GACrD,OAAO,CAYT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAGvE;AAED,8CAA8C;AAC9C,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,mEAAmE;AACnE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,EAAE,CAO7D;AAED,MAAM,WAAW,uBAAwB,SAAQ,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC;CAAG;AAEvF,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,MAAM,EAAE,eAAe,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,uBAA4B,GACpC,gBAAgB,CAelB;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,GAAG,SAAS,CAEpE;AAED,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,GAAG,SAAS,CAK1E;AAED,MAAM,MAAM,sBAAsB,GAC9B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACjD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,MAAM,WAAW,yBAA0B,SAAQ,oBAAoB;IACrE,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,EAC3D,EAAE,GAAG,EAAE,SAAc,EAAE,OAAO,EAAE,EAAE,yBAAyB,GAC1D,OAAO,CAAC,sBAAsB,CAAC,CA4BjC"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-http-browser",
3
- "version": "0.6.5",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "type": "module",
6
- "description": "ServiceWorker-based HTTP server for browsers, with relay and same-origin dispatch modes",
6
+ "description": "ServiceWorker-based HTTP and WebSocket server for browsers: handlers in pages and Dedicated Workers, same-origin and relay modes, streams with backpressure",
7
7
  "bugs": {
8
8
  "url": "https://github.com/statewalker/webrun-wire/issues"
9
9
  },
@@ -40,6 +40,11 @@
40
40
  "types": "./dist/relay-worker.d.ts",
41
41
  "import": "./dist/relay-worker.js"
42
42
  },
43
+ "./worker": {
44
+ "source": "./src/worker.ts",
45
+ "types": "./dist/worker.d.ts",
46
+ "import": "./dist/worker.js"
47
+ },
43
48
  "./sw-worker": {
44
49
  "default": "./dist/sw-worker.js"
45
50
  }
@@ -53,9 +58,9 @@
53
58
  ],
54
59
  "dependencies": {
55
60
  "idb-keyval": "^6.3.0",
56
- "@statewalker/webrun-http-streams": "^0.2.5",
57
- "@statewalker/webrun-streams": "^0.2.2",
58
- "@statewalker/webrun-rpc": "^0.4.3"
61
+ "@statewalker/webrun-streams": "^0.2.3",
62
+ "@statewalker/webrun-rpc": "^0.5.1",
63
+ "@statewalker/webrun-http-streams": "^0.3.1"
59
64
  },
60
65
  "devDependencies": {
61
66
  "@biomejs/biome": "^2.5.15",
@@ -1,4 +1,5 @@
1
1
  import { deserializeError, serializeError } from "@statewalker/webrun-streams";
2
+ import { withDeadline } from "./deadline.js";
2
3
  import type { MessageSink, MessageSource } from "./message-target.js";
3
4
 
4
5
  export type ChannelCallHandler = (
@@ -7,6 +8,34 @@ export type ChannelCallHandler = (
7
8
  ...transfers: MessagePort[]
8
9
  ) => unknown;
9
10
 
11
+ const TRANSFER = Symbol.for("webrun-http-browser:transfer");
12
+
13
+ interface TransferResult {
14
+ [TRANSFER]: true;
15
+ value: unknown;
16
+ transfer: Transferable[];
17
+ }
18
+
19
+ /**
20
+ * A channel call's result that carries transferables, a `MessagePort` say:
21
+ * return `withTransfer({ port }, [port])` from a `handleChannelCalls` handler
22
+ * and `callChannel` resolves with `{ port }`.
23
+ */
24
+ export function withTransfer<T>(value: T, transfer: Transferable[]): T {
25
+ return { [TRANSFER]: true, value, transfer } as unknown as T;
26
+ }
27
+
28
+ function isTransferResult(value: unknown): value is TransferResult {
29
+ return typeof value === "object" && value !== null && TRANSFER in value;
30
+ }
31
+
32
+ /** Splits a handler's result into the value to post and its transfer list. */
33
+ export function unwrapTransfer(result: unknown): { value: unknown; transfer: Transferable[] } {
34
+ return isTransferResult(result)
35
+ ? { value: result.value, transfer: result.transfer }
36
+ : { value: result, transfer: [] };
37
+ }
38
+
10
39
  export function callChannel<T = unknown>(
11
40
  target: MessageSink,
12
41
  callType: string,
@@ -33,14 +62,35 @@ export function handleChannelCalls(
33
62
  if (!event.data || event.data.type !== callType) return;
34
63
  const [port, ...transfers] = event.ports ?? [];
35
64
  const response: { result?: unknown; error?: unknown } = {};
65
+ let transfer: Transferable[] = [];
36
66
  try {
37
- response.result = await handler(event, event.data.params, ...transfers);
67
+ const result = await handler(event, event.data.params, ...transfers);
68
+ ({ value: response.result, transfer } = unwrapTransfer(result));
38
69
  } catch (error) {
39
70
  response.error = serializeError(error);
40
71
  }
41
- port?.postMessage(response);
72
+ port?.postMessage(response, transfer);
42
73
  };
43
74
  target.addEventListener("message", listener);
44
75
  target.start?.();
45
76
  return () => target.removeEventListener("message", listener);
46
77
  }
78
+
79
+ /**
80
+ * `callChannel`, failing after `ms` instead of waiting for ever: a peer that
81
+ * died (a ServiceWorker stopped mid-call) or that does not know `callType` (an
82
+ * older version) never answers.
83
+ */
84
+ export function callChannelWithin<T = unknown>(
85
+ ms: number,
86
+ target: MessageSink,
87
+ callType: string,
88
+ params: unknown,
89
+ ...transfers: Transferable[]
90
+ ): Promise<T> {
91
+ return withDeadline(
92
+ Date.now() + ms,
93
+ callChannel<T>(target, callType, params, ...transfers),
94
+ () => new Error(`No answer to ${callType} within ${ms} ms`),
95
+ );
96
+ }
@@ -26,8 +26,14 @@ export const CLAIM_CALL = "CLAIM";
26
26
  * - `uncontrolled`: the worker is active but the page is not controlled by it.
27
27
  * - `unresponsive`: the page is controlled, but the worker did not answer the
28
28
  * adapter's handshake in time.
29
+ * - `incompatible`: the worker answered, but runs another version of this
30
+ * package's link protocol.
29
31
  */
30
- export type ServiceWorkerControlFailure = "activation-timeout" | "uncontrolled" | "unresponsive";
32
+ export type ServiceWorkerControlFailure =
33
+ | "activation-timeout"
34
+ | "uncontrolled"
35
+ | "unresponsive"
36
+ | "incompatible";
31
37
 
32
38
  /**
33
39
  * Why a page could not get a working ServiceWorker. `reason` says which wait
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
1
  export * from "./core/index.js";
2
2
  export * from "./http/index.js";
3
+ export * from "./link/index.js";
3
4
  export * from "./relay/index.js";
5
+ export * from "./sw/index.js";
6
+ export * from "./ws/index.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Makes `target` disposable with `await using`: installs `dispose` as its
3
+ * `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
4
+ * iterators lose what a native generator gets from `AsyncIteratorPrototype`;
5
+ * this gives it back. A no-op where the symbol is missing. Kept off the
6
+ * declared types, which would require consumers to compile with the
7
+ * `esnext.disposable` lib.
8
+ */
9
+ export function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T {
10
+ // Read here, not at module level: a top-level read is code a bundler keeps.
11
+ const asyncDispose = (Symbol as { asyncDispose?: symbol }).asyncDispose;
12
+ if (asyncDispose && !(asyncDispose in target)) {
13
+ Object.defineProperty(target, asyncDispose, {
14
+ value: async () => {
15
+ await dispose();
16
+ },
17
+ configurable: true,
18
+ writable: true,
19
+ });
20
+ }
21
+ return target;
22
+ }
@@ -0,0 +1,3 @@
1
+ export function errorMessage(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error);
3
+ }