@statewalker/webrun-http-browser 0.3.2 → 0.3.4

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 (92) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +321 -50
  3. package/demo/demo-1.html +109 -194
  4. package/demo/demo-2.html +119 -580
  5. package/dist/core/data-calls.d.ts +5 -0
  6. package/dist/core/data-calls.d.ts.map +1 -0
  7. package/dist/core/data-channels.d.ts +17 -0
  8. package/dist/core/data-channels.d.ts.map +1 -0
  9. package/dist/core/index.d.ts +6 -0
  10. package/dist/core/index.d.ts.map +1 -0
  11. package/dist/core/message-target.d.ts +16 -0
  12. package/dist/core/message-target.d.ts.map +1 -0
  13. package/dist/core/registry.d.ts +18 -0
  14. package/dist/core/registry.d.ts.map +1 -0
  15. package/dist/http/http-send-recieve.d.ts +23 -0
  16. package/dist/http/http-send-recieve.d.ts.map +1 -0
  17. package/dist/http/index.d.ts +4 -0
  18. package/dist/http/index.d.ts.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +2541 -807
  22. package/dist/relay/index-sw.d.ts +7 -0
  23. package/dist/relay/index-sw.d.ts.map +1 -0
  24. package/dist/relay/index.d.ts +69 -0
  25. package/dist/relay/index.d.ts.map +1 -0
  26. package/dist/relay/split-service-url.d.ts +13 -0
  27. package/dist/relay/split-service-url.d.ts.map +1 -0
  28. package/dist/relay-sw.d.ts +2 -0
  29. package/dist/relay-sw.d.ts.map +1 -0
  30. package/dist/relay-sw.js +878 -0
  31. package/dist/sw/http-sw-dispatcher.d.ts +28 -0
  32. package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
  33. package/dist/sw/index.d.ts +3 -0
  34. package/dist/sw/index.d.ts.map +1 -0
  35. package/dist/sw/sw-dispatcher.d.ts +74 -0
  36. package/dist/sw/sw-dispatcher.d.ts.map +1 -0
  37. package/dist/sw-worker.d.ts +2 -0
  38. package/dist/sw-worker.d.ts.map +1 -0
  39. package/dist/sw-worker.js +813 -0
  40. package/dist/sw.d.ts +2 -0
  41. package/dist/sw.d.ts.map +1 -0
  42. package/dist/sw.js +1076 -0
  43. package/package.json +50 -37
  44. package/public/index.html +61 -97
  45. package/public/index.js +7 -4
  46. package/public/sw-worker.js +4 -0
  47. package/public-relay/heartbeat.js +4 -3
  48. package/public-relay/relay-sw.js +4 -6
  49. package/public-relay/relay.html +16 -11
  50. package/src/core/data-calls.ts +46 -0
  51. package/src/core/data-channels.ts +224 -0
  52. package/src/core/index.ts +9 -0
  53. package/src/core/message-target.ts +18 -0
  54. package/src/core/registry.ts +54 -0
  55. package/src/http/http-send-recieve.ts +81 -0
  56. package/src/http/index.ts +7 -0
  57. package/src/index.ts +3 -0
  58. package/src/relay/index-sw.ts +145 -0
  59. package/src/relay/index.ts +261 -0
  60. package/src/relay/split-service-url.ts +30 -0
  61. package/src/relay-sw.ts +7 -0
  62. package/src/sw/http-sw-dispatcher.ts +122 -0
  63. package/src/sw/index.ts +2 -0
  64. package/src/sw/sw-dispatcher.ts +307 -0
  65. package/src/sw-worker.ts +7 -0
  66. package/src/sw.ts +1 -0
  67. package/CHANGELOG.md +0 -1
  68. package/demo/demo-3.html +0 -167
  69. package/dist/index-sw-umd.js +0 -798
  70. package/dist/index-sw-umd.min.js +0 -2
  71. package/dist/index-sw.js +0 -791
  72. package/dist/index-sw.min.js +0 -2
  73. package/dist/index-umd.js +0 -853
  74. package/dist/index-umd.min.js +0 -2
  75. package/dist/index.min.js +0 -2
  76. package/index.js +0 -1
  77. package/src/core/data-calls.js +0 -36
  78. package/src/core/data-channels.js +0 -192
  79. package/src/core/data-send-recieve.js +0 -28
  80. package/src/core/errors.js +0 -13
  81. package/src/core/index.js +0 -4
  82. package/src/http/HttpError.js +0 -59
  83. package/src/http/http-send-recieve.js +0 -40
  84. package/src/http/http-stubs.js +0 -135
  85. package/src/http/index.js +0 -3
  86. package/src/http/readable-streams.js +0 -33
  87. package/src/index.js +0 -7
  88. package/src/relay/index-sw.js +0 -216
  89. package/src/relay/index.js +0 -263
  90. package/src/relay/splitServiceUrl.js +0 -24
  91. package/src/sw/http-sw-dispatcher.js +0 -128
  92. package/src/sw/sw-dispatcher.js +0 -264
package/package.json CHANGED
@@ -1,56 +1,69 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-http-browser",
3
- "version": "0.3.2",
4
- "description": "Service-worker-based HTTP server simulation for browsers",
5
- "keywords": [],
6
- "homepage": "https://github.com/statewalker/statewalker-webrun-http-browser",
3
+ "version": "0.3.4",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "ServiceWorker-based HTTP server for browsers, with relay and same-origin dispatch modes",
7
+ "homepage": "https://github.com/statewalker/webrun-wire",
7
8
  "author": {
8
9
  "name": "Mikhail Kotelnikov",
9
10
  "email": "mikhail.kotelnikov@gmail.com"
10
11
  },
11
12
  "license": "MIT",
12
- "type": "module",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git@github.com:statewalker/webrun-wire.git"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "module": "./dist/index.js",
19
+ "types": "./dist/index.d.ts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ },
25
+ "./sw": {
26
+ "types": "./dist/sw.d.ts",
27
+ "import": "./dist/sw.js"
28
+ },
29
+ "./relay-sw": {
30
+ "default": "./dist/relay-sw.js"
31
+ },
32
+ "./sw-worker": {
33
+ "default": "./dist/sw-worker.js"
34
+ }
35
+ },
13
36
  "files": [
14
- "dist/**/package.json",
15
- "dist/**/*.js",
37
+ "dist",
38
+ "src",
16
39
  "public",
17
40
  "public-relay",
18
- "src/**/*.js",
19
- "demo",
20
- "index.js"
41
+ "demo"
21
42
  ],
22
- "module": "src/index.js",
23
- "main": "src/index.js",
24
- "jsdelivr": "dist/index.js",
25
- "unpkg": "dist/index.js",
26
- "exports": {
27
- "umd": "./dist/index-umd.min.js",
28
- "default": "./src/index.js"
29
- },
30
43
  "dependencies": {
31
- "@statewalker/utils": "^0.4.2",
32
- "idb-keyval": "^6.2.0"
44
+ "idb-keyval": "^6.3.0",
45
+ "@statewalker/webrun-streams": "0.1.1",
46
+ "@statewalker/webrun-http-streams": "0.2.1"
33
47
  },
34
48
  "devDependencies": {
35
- "@statewalker/rollup": "^0.1.6",
36
- "eslint": "^8",
37
- "expect.js": "^0.3",
38
- "mocha": "^10",
39
- "rollup": "^3"
40
- },
41
- "repository": {
42
- "type": "git",
43
- "url": "git@github.com:statewalker/statewalker-webrun-http-browser.git"
44
- },
45
- "scripts": {
46
- "eslint": "eslint src",
47
- "rollup": "rollup -c",
48
- "watch": "rollup -c --watch",
49
- "test": "mocha -R spec ./test/index.js",
50
- "prepublishOnly": "rm -rf dist && yarn test && yarn rollup"
49
+ "@types/node": "^26.2.0",
50
+ "http-server": "^14.1.1",
51
+ "rimraf": "^6.1.3",
52
+ "rolldown": "^1.2.4",
53
+ "typescript": "^7.0.2",
54
+ "vitest": "^4.1.10"
51
55
  },
52
56
  "sideEffects": false,
53
57
  "publishConfig": {
54
58
  "access": "public"
59
+ },
60
+ "scripts": {
61
+ "build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
62
+ "test": "vitest run",
63
+ "lint": "biome check src tests",
64
+ "serve": "http-server -p 5173 -c-1 -s .",
65
+ "example:same-origin": "pnpm run build && http-server -p 5173 -c-1 -o /public/index.html .",
66
+ "example:relay-site": "pnpm run build && http-server -p 5173 -c-1 -o /demo/demo-1.html .",
67
+ "example:relay-files": "pnpm run build && http-server -p 5173 -c-1 -o /demo/demo-2.html ."
55
68
  }
56
- }
69
+ }
package/public/index.html CHANGED
@@ -1,100 +1,64 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
-
4
- <head>
5
- <meta charset='utf-8'>
6
- <meta http-equiv='X-UA-Compatible' content='IE=edge'>
7
- <title>Page Title</title>
8
- <meta name='viewport' content='width=device-width, initial-scale=1'>
9
- </head>
10
-
11
- <body>
12
- <div id="info"></div>
13
- <div id="messages" style="display: block; border: 1px solid blue; padding: 0.5em;"></div>
14
- <div id="counter"></div>
15
-
16
- <button id="button">Load Data</button>
17
- <div id="panel"></div>
18
-
19
- <script type="module">
20
- import { v4 as uuidv4 } from "https://unpkg.com/uuid@9.0.0/dist/esm-browser/index.js";
21
- import * as relayLib from "../dist/index.js";
22
- import { callPeriodically } from "../public-relay/heartbeat.js";
23
-
24
- document.addEventListener("DOMContentLoaded", main);
25
- // Promise.resolve().then(main).catch(console.error);
26
-
27
- async function main() {
28
-
29
- const key = uuidv4();
30
- const [register, cleanup] = relayLib.newRegistry();
31
-
32
- const options = {
33
- // url: new URL(`../public-relay/relay.html?${Math.random()}`, import.meta.url),
34
- // container: document.body,
35
- };
36
- const serviceKey = "abc";
37
-
38
- // ---------------------------
39
- // Server-side part
40
- const serverPort = await relayLib.newRemoteRelayChannel(options);
41
- register(serverPort.close);
42
-
43
- const serviceHandler = async (request) => {
44
- const info = relayLib.splitServiceUrl(request.url);
45
-
46
- const result = {
47
- message: "Hello Wonderful World!",
48
- ...info
49
- }
50
- return new Response(JSON.stringify(result, null, 2), {
51
- headers: {
52
- "Content-Type": "application/json"
53
- }
54
- })
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>webrun-http-browser — same-origin demo</title>
7
+ <style>
8
+ body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 48rem; }
9
+ h1 { margin-top: 0; }
10
+ pre { background: #f4f4f5; padding: 0.5rem 1rem; border-radius: 0.25rem; overflow: auto; }
11
+ .log { border: 1px solid #e5e5e5; padding: 0.5rem 1rem; min-height: 4rem; }
12
+ .log p { margin: 0.25rem 0; font-family: monospace; }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <h1>Same-origin ServiceWorker HTTP demo</h1>
17
+ <p>
18
+ This page registers a ServiceWorker at <code>./sw-worker.js</code> and
19
+ mounts a tiny in-browser HTTP handler under <code>./api/</code>. The SW
20
+ then intercepts same-origin <code>fetch()</code> calls matching that
21
+ prefix and routes them to the handler — no remote server involved.
22
+ </p>
23
+ <div class="log" id="log"></div>
24
+
25
+ <script type="module">
26
+ import { createLocalAdapter } from "./index.js";
27
+
28
+ const log = (msg) => {
29
+ const p = document.createElement("p");
30
+ p.textContent = msg;
31
+ document.querySelector("#log").appendChild(p);
55
32
  };
56
- register(await relayLib.initHttpService(serviceHandler, {
57
- key: serviceKey,
58
- port: serverPort.port
59
- }))
60
-
61
- // ---------------------------
62
- // Call the defined endpoint.
63
-
64
- // We can not use the "fech" method - to be able
65
- // to use it this page should be served from the same root URL
66
- // as the service worker managing pages.
67
- // To deal with this constraint we use the connection
68
- // port allowing to perform direct calls using
69
- // the explicit service key on the client.
70
- const clientPort = await relayLib.newRemoteRelayChannel(options);
71
- register(clientPort.close);
72
-
73
- const request = new Request(`http://www.foo.bar/titi/toto`);
74
- const response = await relayLib.callHttpService(request, {
75
- key: serviceKey,
76
- port: clientPort.port
77
- });
78
- const json = await response.json();
79
- console.log('* Response:', json);
80
-
81
-
82
- // // ---------------------------
83
- // // Start hearbeting calls - to avoid removal of the underlying ServiceWorker
84
- // // const url = new URL(`../public-relay/~.ping`, import.meta.url);
85
- // register(callPeriodically(async () => {
86
- // const response = await relayLib.callHttpService(new Request("~sys/ping/"), {
87
- // key: serviceKey,
88
- // port: clientPort.port
89
- // })
90
- // const result = await response.json();
91
- // console.log(result);
92
- // }, 10000))
93
-
94
- }
95
-
96
- </script>
97
-
98
- </body>
99
33
 
100
- </html>
34
+ // The adapter key is also the first URL segment the SW routes to this page;
35
+ // registered handler prefixes must live under `<key>/…`.
36
+ const KEY = "demo";
37
+ const adapter = createLocalAdapter({ key: KEY });
38
+ try {
39
+ await adapter.start();
40
+ log("ServiceWorker activated.");
41
+
42
+ const { baseUrl } = await adapter.register(`${KEY}/api/`, async (request) => {
43
+ const url = new URL(request.url);
44
+ return new Response(
45
+ JSON.stringify({
46
+ message: "Hello from the in-browser HTTP handler!",
47
+ at: url.pathname,
48
+ time: new Date().toISOString(),
49
+ }),
50
+ { headers: { "Content-Type": "application/json" } },
51
+ );
52
+ });
53
+ log(`Handler registered at ${baseUrl}`);
54
+
55
+ const res = await fetch(`${baseUrl}anything`);
56
+ const body = await res.json();
57
+ log(`GET ${baseUrl}anything → ${JSON.stringify(body)}`);
58
+ } catch (error) {
59
+ log(`Error: ${error.message}`);
60
+ console.error(error);
61
+ }
62
+ </script>
63
+ </body>
64
+ </html>
package/public/index.js CHANGED
@@ -1,9 +1,12 @@
1
- import { SwHttpAdapter } from "../dist/index.js";
1
+ import { SwHttpAdapter } from "../dist/sw.js";
2
2
 
3
- export default function (options) {
3
+ /**
4
+ * Factory for a same-origin `SwHttpAdapter` rooted at this directory.
5
+ * Apps pass the `key` they want to expose and any per-handler options.
6
+ */
7
+ export function createLocalAdapter(options) {
4
8
  return new SwHttpAdapter({
5
- serviceWorkerUrl: new URL("./index-service-worker.js", import.meta.url) +
6
- "",
9
+ serviceWorkerUrl: `${new URL("./sw-worker.js", import.meta.url)}`,
7
10
  ...options,
8
11
  });
9
12
  }
@@ -0,0 +1,4 @@
1
+ // Same-origin HTTP dispatcher ServiceWorker.
2
+ // Must be served from the same directory as the app pages so its scope
3
+ // (`./`) covers them.
4
+ importScripts("../dist/sw-worker.js");
@@ -5,19 +5,20 @@ export function startHeartbit({
5
5
  url = "./ping", // 10 seconds
6
6
  }) {
7
7
  return callPeriodically(async () => {
8
- const res = await fetch(url);
8
+ const _res = await fetch(url);
9
9
  }, timeout);
10
10
  }
11
11
 
12
12
  export async function callPeriodically(action, timeout) {
13
- let timerId, stopped = false;
13
+ let timerId,
14
+ stopped = false;
14
15
  async function run() {
15
16
  try {
16
17
  await action();
17
18
  } catch (err) {
18
19
  console.error(err);
19
20
  }
20
- timerId = (!stopped) ? workerTimer.setTimeout(run, timeout) : 0;
21
+ timerId = !stopped ? workerTimer.setTimeout(run, timeout) : 0;
21
22
  }
22
23
  run();
23
24
  return () => {
@@ -1,6 +1,4 @@
1
- // It is important to include the Service Worker in this file:
2
- // the service worker controls all resources starting from the
3
- // base URL of the running script.
4
- // By including the script directly (without this file) you change
5
- // the base URL of the controlled resources, and it is not what you want!
6
- importScripts("../dist/index-sw.js");
1
+ // The relay ServiceWorker.
2
+ // It MUST be served from this directory so its scope (`./`) covers
3
+ // relay.html and any in-browser `fetch()` calls the apps route through it.
4
+ importScripts("../dist/relay-sw.js");
@@ -1,12 +1,17 @@
1
1
  <!DOCTYPE html>
2
- <html>
3
- <body>
4
- <script type="module">
5
- import { getRelayWindowMessageHandler } from "../dist/index.js";
6
- window.onmessage = getRelayWindowMessageHandler({
7
- swUrl : new URL("./relay-sw.js", import.meta.url) + "",
8
- scopeUrl : new URL("./", import.meta.url) + "",
9
- });
10
- </script>
11
- </body>
12
- </html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>webrun-http-browser relay</title>
6
+ </head>
7
+ <body>
8
+ <script type="module">
9
+ import { getRelayWindowMessageHandler } from "../dist/index.js";
10
+
11
+ window.onmessage = getRelayWindowMessageHandler({
12
+ swUrl: `${new URL("./relay-sw.js", import.meta.url)}`,
13
+ scopeUrl: `${new URL("./", import.meta.url)}`,
14
+ });
15
+ </script>
16
+ </body>
17
+ </html>
@@ -0,0 +1,46 @@
1
+ import { deserializeError, serializeError } from "@statewalker/webrun-streams";
2
+ import type { MessageSink, MessageSource } from "./message-target.js";
3
+
4
+ export type ChannelCallHandler = (
5
+ event: MessageEvent,
6
+ params: unknown,
7
+ ...transfers: MessagePort[]
8
+ ) => unknown;
9
+
10
+ export function callChannel<T = unknown>(
11
+ target: MessageSink,
12
+ callType: string,
13
+ params: unknown,
14
+ ...transfers: Transferable[]
15
+ ): Promise<T> {
16
+ return new Promise<T>((resolve, reject) => {
17
+ const channel = new MessageChannel();
18
+ channel.port1.onmessage = (ev: MessageEvent) => {
19
+ const { result, error } = ev.data as { result?: T; error?: unknown };
20
+ if (error) reject(deserializeError(error as never));
21
+ else resolve(result as T);
22
+ };
23
+ target.postMessage({ type: callType, params }, [channel.port2, ...transfers]);
24
+ });
25
+ }
26
+
27
+ export function handleChannelCalls(
28
+ target: MessageSource,
29
+ callType: string,
30
+ handler: ChannelCallHandler,
31
+ ): () => void {
32
+ const listener = async (event: MessageEvent) => {
33
+ if (!event.data || event.data.type !== callType) return;
34
+ const [port, ...transfers] = event.ports ?? [];
35
+ const response: { result?: unknown; error?: unknown } = {};
36
+ try {
37
+ response.result = await handler(event, event.data.params, ...transfers);
38
+ } catch (error) {
39
+ response.error = serializeError(error);
40
+ }
41
+ port?.postMessage(response);
42
+ };
43
+ target.addEventListener("message", listener);
44
+ target.start?.();
45
+ return () => target.removeEventListener("message", listener);
46
+ }
@@ -0,0 +1,224 @@
1
+ import {
2
+ deserializeError,
3
+ recieveIterator,
4
+ type SerializedError,
5
+ sendIterator,
6
+ serializeError,
7
+ } from "@statewalker/webrun-streams";
8
+ import type { MessageTarget } from "./message-target.js";
9
+
10
+ const MESSAGE_TYPE_REQUEST = "REQUEST";
11
+ const MESSAGE_TYPE_RESPONSE = "RESPONSE";
12
+
13
+ type InvocationMessage =
14
+ | { type: typeof MESSAGE_TYPE_REQUEST; callId: number; request: unknown }
15
+ | {
16
+ type: typeof MESSAGE_TYPE_RESPONSE;
17
+ callId: number;
18
+ response?: unknown;
19
+ error?: SerializedError;
20
+ };
21
+
22
+ type PendingRequest = {
23
+ resolve: (value: unknown) => void;
24
+ reject: (reason: unknown) => void;
25
+ };
26
+
27
+ let __invocationCounter = 0;
28
+
29
+ export interface InvocationChannel {
30
+ start(): Promise<void>;
31
+ close(): Promise<void>;
32
+ invoke<T = unknown>(request?: unknown, ...transfers: Transferable[]): Promise<T>;
33
+ }
34
+
35
+ export interface NewInvocationChannelOptions {
36
+ port: MessageTarget;
37
+ handler?: (request: unknown, ...ports: MessagePort[]) => unknown;
38
+ onError?: (error: unknown) => void;
39
+ newCallId?: () => number;
40
+ }
41
+
42
+ export function newInvokationChannel({
43
+ port,
44
+ handler = () => {
45
+ throw new Error("Handler not implemented");
46
+ },
47
+ onError = console.error,
48
+ newCallId = () => ++__invocationCounter,
49
+ }: NewInvocationChannelOptions): InvocationChannel {
50
+ const requests: Record<number, PendingRequest> = {};
51
+
52
+ const listener = async (event: MessageEvent) => {
53
+ const data = (event.data ?? {}) as InvocationMessage;
54
+ if (data.type === MESSAGE_TYPE_REQUEST) {
55
+ try {
56
+ const result = await handler(data.request, ...(event.ports as MessagePort[]));
57
+ const [response, ...transfers] = Array.isArray(result)
58
+ ? (result as unknown[])
59
+ : result !== undefined
60
+ ? [result]
61
+ : [];
62
+ port.postMessage(
63
+ { type: MESSAGE_TYPE_RESPONSE, callId: data.callId, response },
64
+ transfers as Transferable[],
65
+ );
66
+ } catch (error) {
67
+ port.postMessage({
68
+ type: MESSAGE_TYPE_RESPONSE,
69
+ callId: data.callId,
70
+ error: serializeError(error),
71
+ });
72
+ }
73
+ } else if (data.type === MESSAGE_TYPE_RESPONSE) {
74
+ const pending = requests[data.callId];
75
+ delete requests[data.callId];
76
+ if (!pending) return;
77
+ if (data.error) pending.reject(deserializeError(data.error));
78
+ else pending.resolve(data.response);
79
+ }
80
+ };
81
+
82
+ const start = async () => {
83
+ try {
84
+ port.addEventListener("message", listener);
85
+ await port.start?.();
86
+ } catch (e) {
87
+ onError(e);
88
+ }
89
+ };
90
+
91
+ const close = async () => {
92
+ try {
93
+ port.removeEventListener("message", listener);
94
+ await port.close?.();
95
+ } catch (e) {
96
+ onError(e);
97
+ }
98
+ };
99
+
100
+ const invoke = <T>(request: unknown = {}, ...transfers: Transferable[]): Promise<T> => {
101
+ const callId = newCallId();
102
+ return new Promise<T>((resolve, reject) => {
103
+ try {
104
+ requests[callId] = {
105
+ resolve: resolve as (value: unknown) => void,
106
+ reject,
107
+ };
108
+ port.postMessage({ type: MESSAGE_TYPE_REQUEST, callId, request }, transfers);
109
+ } catch (error) {
110
+ delete requests[callId];
111
+ reject(error);
112
+ }
113
+ });
114
+ };
115
+
116
+ return { start, close, invoke };
117
+ }
118
+
119
+ export async function* sendStream<T>(
120
+ communicationPort: MessageTarget,
121
+ input: AsyncIterable<T>,
122
+ params: Record<string, unknown> = {},
123
+ ): AsyncGenerator<T, void, unknown> {
124
+ const messageChannel = new MessageChannel();
125
+ communicationPort.postMessage({ type: "START_CALL", params }, [messageChannel.port2]);
126
+
127
+ const channel = newStreamChannel<T>(messageChannel.port1);
128
+ try {
129
+ await channel.start();
130
+ void channel.sendAll(input);
131
+ yield* channel.recieveAll();
132
+ } finally {
133
+ await channel.close();
134
+ }
135
+ }
136
+
137
+ export type StreamHandler<T> = (
138
+ input: AsyncIterable<T>,
139
+ params: Record<string, unknown>,
140
+ ) => AsyncIterable<T> | Promise<AsyncIterable<T>>;
141
+
142
+ export function handleStreams<T>(
143
+ communicationPort: MessageTarget,
144
+ handler: StreamHandler<T>,
145
+ ): () => void {
146
+ const listener = async (event: MessageEvent) => {
147
+ const { type, params } = (event.data ?? {}) as {
148
+ type?: string;
149
+ params?: Record<string, unknown>;
150
+ };
151
+ if (type !== "START_CALL") return;
152
+ const port = event.ports[0];
153
+ const channel = newStreamChannel<T>(port);
154
+ try {
155
+ await channel.start();
156
+ const input = channel.recieveAll();
157
+ const response = await handler(input, params ?? {});
158
+ await channel.sendAll(response);
159
+ } finally {
160
+ await channel.close();
161
+ }
162
+ };
163
+ communicationPort.addEventListener("message", listener);
164
+ communicationPort.start?.();
165
+ return () => communicationPort.removeEventListener("message", listener);
166
+ }
167
+
168
+ interface StreamChannel<T> {
169
+ start(): Promise<void>;
170
+ close(): Promise<void>;
171
+ recieveAll(): AsyncGenerator<T, void, unknown>;
172
+ sendAll(it: AsyncIterable<T>): Promise<void>;
173
+ }
174
+
175
+ function newStreamChannel<T>(port: MessageTarget): StreamChannel<T> {
176
+ type DataListener = (msg: { done?: boolean; value?: T; error?: unknown }) => Promise<boolean>;
177
+ let listeners: DataListener[] = [];
178
+ let iterators: AsyncIterable<T>[] = [];
179
+
180
+ const notifyAll = async (data: { done?: boolean; value?: T; error?: unknown }) => {
181
+ for (const listener of listeners) await listener(data);
182
+ };
183
+
184
+ const channel = newInvokationChannel({
185
+ port,
186
+ handler: (data: unknown) => notifyAll(data as { done?: boolean; value?: T; error?: unknown }),
187
+ });
188
+
189
+ const start = () => channel.start();
190
+
191
+ const close = async () => {
192
+ await notifyAll({ done: true });
193
+ for (const it of [...iterators]) {
194
+ const iterable = it as AsyncIterable<T> & { return?: () => unknown };
195
+ await iterable.return?.();
196
+ }
197
+ await channel.close();
198
+ };
199
+
200
+ async function* recieveAll(): AsyncGenerator<T, void, unknown> {
201
+ yield* recieveIterator<T>((deliver) => {
202
+ listeners.push(deliver as DataListener);
203
+ return () => {
204
+ listeners = listeners.filter((l) => l !== deliver);
205
+ };
206
+ });
207
+ }
208
+
209
+ async function sendAll(it: AsyncIterable<T>): Promise<void> {
210
+ await sendIterator<T>(
211
+ (chunk) => void channel.invoke(chunk),
212
+ (async function* () {
213
+ try {
214
+ iterators.push(it);
215
+ yield* it;
216
+ } finally {
217
+ iterators = iterators.filter((i) => i !== it);
218
+ }
219
+ })(),
220
+ );
221
+ }
222
+
223
+ return { start, close, recieveAll, sendAll };
224
+ }
@@ -0,0 +1,9 @@
1
+ // Re-export the stream / iterator / error primitives so existing consumers
2
+ // of `@statewalker/webrun-http-browser` keep working after the
3
+ // `webrun-streams` extraction.
4
+ export * from "@statewalker/webrun-streams";
5
+
6
+ export * from "./data-calls.js";
7
+ export * from "./data-channels.js";
8
+ export * from "./message-target.js";
9
+ export * from "./registry.js";
@@ -0,0 +1,18 @@
1
+ export type MessageListener = (event: MessageEvent) => void | Promise<void>;
2
+
3
+ /** An object we can listen for `"message"` events on. */
4
+ export interface MessageSource {
5
+ addEventListener(type: "message", listener: MessageListener): void;
6
+ removeEventListener(type: "message", listener: MessageListener): void;
7
+ start?(): void | Promise<void>;
8
+ }
9
+
10
+ /** An object we can post messages to (with optional transferable list). */
11
+ export interface MessageSink {
12
+ postMessage(message: unknown, transfer?: Transferable[]): void;
13
+ }
14
+
15
+ /** Full-duplex message target: both sends and receives. */
16
+ export interface MessageTarget extends MessageSource, MessageSink {
17
+ close?(): void | Promise<void>;
18
+ }