@statewalker/webrun-http-browser 0.3.3 → 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.
- package/LICENSE +1 -1
- package/README.md +321 -51
- package/demo/demo-1.html +109 -194
- package/demo/demo-2.html +118 -299
- package/dist/core/data-calls.d.ts +5 -0
- package/dist/core/data-calls.d.ts.map +1 -0
- package/dist/core/data-channels.d.ts +17 -0
- package/dist/core/data-channels.d.ts.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/message-target.d.ts +16 -0
- package/dist/core/message-target.d.ts.map +1 -0
- package/dist/core/registry.d.ts +18 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/http/http-send-recieve.d.ts +23 -0
- package/dist/http/http-send-recieve.d.ts.map +1 -0
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2541 -807
- package/dist/relay/index-sw.d.ts +7 -0
- package/dist/relay/index-sw.d.ts.map +1 -0
- package/dist/relay/index.d.ts +69 -0
- package/dist/relay/index.d.ts.map +1 -0
- package/dist/relay/split-service-url.d.ts +13 -0
- package/dist/relay/split-service-url.d.ts.map +1 -0
- package/dist/relay-sw.d.ts +2 -0
- package/dist/relay-sw.d.ts.map +1 -0
- package/dist/relay-sw.js +878 -0
- package/dist/sw/http-sw-dispatcher.d.ts +28 -0
- package/dist/sw/http-sw-dispatcher.d.ts.map +1 -0
- package/dist/sw/index.d.ts +3 -0
- package/dist/sw/index.d.ts.map +1 -0
- package/dist/sw/sw-dispatcher.d.ts +74 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -0
- package/dist/sw-worker.d.ts +2 -0
- package/dist/sw-worker.d.ts.map +1 -0
- package/dist/sw-worker.js +813 -0
- package/dist/sw.d.ts +2 -0
- package/dist/sw.d.ts.map +1 -0
- package/dist/sw.js +1076 -0
- package/package.json +50 -37
- package/public/index.html +61 -97
- package/public/index.js +7 -4
- package/public/sw-worker.js +4 -0
- package/public-relay/heartbeat.js +4 -3
- package/public-relay/relay-sw.js +4 -6
- package/public-relay/relay.html +16 -11
- package/src/core/data-calls.ts +46 -0
- package/src/core/data-channels.ts +224 -0
- package/src/core/index.ts +9 -0
- package/src/core/message-target.ts +18 -0
- package/src/core/registry.ts +54 -0
- package/src/http/http-send-recieve.ts +81 -0
- package/src/http/index.ts +7 -0
- package/src/index.ts +3 -0
- package/src/relay/index-sw.ts +145 -0
- package/src/relay/index.ts +261 -0
- package/src/relay/split-service-url.ts +30 -0
- package/src/relay-sw.ts +7 -0
- package/src/sw/http-sw-dispatcher.ts +122 -0
- package/src/sw/index.ts +2 -0
- package/src/sw/sw-dispatcher.ts +307 -0
- package/src/sw-worker.ts +7 -0
- package/src/sw.ts +1 -0
- package/CHANGELOG.md +0 -1
- package/demo/demo-2 copy.html +0 -598
- package/demo/demo-3.html +0 -167
- package/dist/index-sw-umd.js +0 -798
- package/dist/index-sw-umd.min.js +0 -2
- package/dist/index-sw.js +0 -791
- package/dist/index-sw.min.js +0 -2
- package/dist/index-umd.js +0 -853
- package/dist/index-umd.min.js +0 -2
- package/dist/index.min.js +0 -2
- package/index.js +0 -1
- package/src/core/data-calls.js +0 -36
- package/src/core/data-channels.js +0 -192
- package/src/core/data-send-recieve.js +0 -28
- package/src/core/errors.js +0 -13
- package/src/core/index.js +0 -4
- package/src/http/HttpError.js +0 -59
- package/src/http/http-send-recieve.js +0 -40
- package/src/http/http-stubs.js +0 -135
- package/src/http/index.js +0 -3
- package/src/http/readable-streams.js +0 -33
- package/src/index.js +0 -7
- package/src/relay/index-sw.js +0 -216
- package/src/relay/index.js +0 -263
- package/src/relay/splitServiceUrl.js +0 -24
- package/src/sw/http-sw-dispatcher.js +0 -128
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
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
|
-
"
|
|
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
|
|
15
|
-
"
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
16
39
|
"public",
|
|
17
40
|
"public-relay",
|
|
18
|
-
"
|
|
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
|
-
"
|
|
32
|
-
"
|
|
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
|
-
"@
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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
|
-
<
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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/
|
|
1
|
+
import { SwHttpAdapter } from "../dist/sw.js";
|
|
2
2
|
|
|
3
|
-
|
|
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("./
|
|
6
|
-
"",
|
|
9
|
+
serviceWorkerUrl: `${new URL("./sw-worker.js", import.meta.url)}`,
|
|
7
10
|
...options,
|
|
8
11
|
});
|
|
9
12
|
}
|
|
@@ -5,19 +5,20 @@ export function startHeartbit({
|
|
|
5
5
|
url = "./ping", // 10 seconds
|
|
6
6
|
}) {
|
|
7
7
|
return callPeriodically(async () => {
|
|
8
|
-
const
|
|
8
|
+
const _res = await fetch(url);
|
|
9
9
|
}, timeout);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export async function callPeriodically(action, timeout) {
|
|
13
|
-
let timerId,
|
|
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 =
|
|
21
|
+
timerId = !stopped ? workerTimer.setTimeout(run, timeout) : 0;
|
|
21
22
|
}
|
|
22
23
|
run();
|
|
23
24
|
return () => {
|
package/public-relay/relay-sw.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
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");
|
package/public-relay/relay.html
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|