@webreflection/bindings 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -14
- package/package.json +10 -7
- package/src/message-port.d.ts +23 -0
- package/src/message-port.js +25 -11
package/README.md
CHANGED
|
@@ -7,18 +7,19 @@ A (soon to be) collection of bindings for various scenarios.
|
|
|
7
7
|
```js
|
|
8
8
|
import bindings from '@webreflection/bindings/message-port';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
/** @typedef {import('@webreflection/bindings/message-port').RemoteProxy} RemoteProxy */
|
|
11
|
+
|
|
12
|
+
const { port1, port2 } = new MessageChannel();
|
|
13
|
+
|
|
14
|
+
// `local` is inferred from the object below.
|
|
15
|
+
// `remote` calls methods exposed on the other side of the port — declare that
|
|
16
|
+
// shape explicitly; it cannot be inferred from `local`.
|
|
17
|
+
/** @type {RemoteProxy<{ sum: (a: number, b: number) => number }>} */
|
|
18
|
+
const remote = bindings(port1, {
|
|
19
|
+
log(...values) {
|
|
20
|
+
console.log(...values);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
22
23
|
|
|
23
24
|
// assuming this is an iframe
|
|
24
25
|
parent.postMessage(null, '*', [port2]);
|
|
@@ -27,8 +28,42 @@ parent.postMessage(null, '*', [port2]);
|
|
|
27
28
|
const value = await remote.sum(1, 2);
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
Each side calls `bindings(port, local)` with its own port and local handlers. The returned proxy always targets the *other* side's `local` object. This module only orchestrates the `postMessage` protocol.
|
|
32
|
+
|
|
33
|
+
#### Typing
|
|
34
|
+
|
|
35
|
+
| What | Known at call site? |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| Second argument (`local`) | yes — inferred from the object you pass |
|
|
38
|
+
| Return value (remote proxy) | no — depends on the other context (worker, iframe, another window) |
|
|
39
|
+
|
|
40
|
+
Without an explicit remote type, TypeScript falls back to a loose string-keyed proxy (`RemoteProxy` defaults to `Record<string, Handler>`). For a known remote API, pick one of:
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
// assertion (JavaScript / checkJs)
|
|
44
|
+
const remote = /** @type {RemoteProxy<{ sum: (a: number, b: number) => number }>} */ (
|
|
45
|
+
bindings(port1, { log() {} })
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// generic parameter (TypeScript)
|
|
51
|
+
import bindings, { type RemoteProxy } from '@webreflection/bindings/message-port';
|
|
52
|
+
|
|
53
|
+
type MainAPI = { log: (...values: unknown[]) => void };
|
|
54
|
+
type WorkerAPI = { sum: (a: number, b: number) => number };
|
|
55
|
+
|
|
56
|
+
const worker = bindings<MainAPI, WorkerAPI>(port1, {
|
|
57
|
+
log(...values) {
|
|
58
|
+
console.log(...values);
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await worker.sum(1, 2);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Remote methods always return a `Promise`, even when the handler on the other side is synchronous.
|
|
31
66
|
|
|
32
67
|
## Currently Available
|
|
33
68
|
|
|
34
|
-
* `@webreflection/bindings/port`
|
|
69
|
+
* `@webreflection/bindings/message-port` — bind APIs over a *MessagePort* (workers, iframes, `MessageChannel`, etc.)
|
package/package.json
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webreflection/bindings",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "A (soon to be) collection of bindings for various scenarios.",
|
|
5
|
-
"
|
|
6
|
-
"./message-port": "./src/message-port.
|
|
5
|
+
"types": {
|
|
6
|
+
"./message-port": "./src/message-port.d.ts"
|
|
7
7
|
},
|
|
8
|
-
"
|
|
9
|
-
"
|
|
8
|
+
"exports": {
|
|
9
|
+
"./message-port": {
|
|
10
|
+
"types": "./src/message-port.d.ts",
|
|
11
|
+
"import": "./src/message-port.js"
|
|
12
|
+
}
|
|
10
13
|
},
|
|
11
14
|
"keywords": [
|
|
15
|
+
"bindings",
|
|
12
16
|
"MessagePort"
|
|
13
17
|
],
|
|
14
18
|
"author": "Andrea Giammarchi",
|
|
@@ -16,10 +20,9 @@
|
|
|
16
20
|
"type": "module",
|
|
17
21
|
"dependencies": {
|
|
18
22
|
"@webreflection/utils": "^0.3.6",
|
|
19
|
-
"next-resolver": "^0.2.
|
|
23
|
+
"next-resolver": "^0.2.1"
|
|
20
24
|
},
|
|
21
25
|
"main": "index.js",
|
|
22
|
-
"devDependencies": {},
|
|
23
26
|
"repository": {
|
|
24
27
|
"type": "git",
|
|
25
28
|
"url": "git+https://github.com/WebReflection/bindings.git"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Any function exposed as a local binding (sync or async). */
|
|
2
|
+
export type Handler = (...args: any[]) => any;
|
|
3
|
+
|
|
4
|
+
/** Map of method names to handlers exposed on this side of the port. */
|
|
5
|
+
export type LocalBindings = Record<string, Handler>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Proxy for methods exposed on the remote side.
|
|
9
|
+
* Every remote call is asynchronous, even when the remote handler is sync.
|
|
10
|
+
*/
|
|
11
|
+
export type RemoteProxy<T extends LocalBindings = LocalBindings> = {
|
|
12
|
+
[K in keyof T]: (...args: Parameters<T[K]>) => Promise<Awaited<ReturnType<T[K]>>>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
declare function bindings<
|
|
16
|
+
Local extends LocalBindings,
|
|
17
|
+
Remote extends LocalBindings = LocalBindings,
|
|
18
|
+
>(
|
|
19
|
+
port: MessagePort,
|
|
20
|
+
local: Local,
|
|
21
|
+
): RemoteProxy<Remote>;
|
|
22
|
+
|
|
23
|
+
export default bindings;
|
package/src/message-port.js
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
1
3
|
import nextResolver from 'next-resolver';
|
|
2
4
|
import { nil } from '@webreflection/utils/empty';
|
|
3
5
|
|
|
4
6
|
const [next, resolve] = nextResolver();
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
* Creates a proxy
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Creates a proxy for calling methods exposed on the remote side of the port,
|
|
10
|
+
* while dispatching incoming calls to `local` on this side.
|
|
11
|
+
*
|
|
12
|
+
* `local` is inferred from the second argument. The remote API is never known
|
|
13
|
+
* from that object alone: pass a second generic or assert the return type
|
|
14
|
+
* (see README).
|
|
15
|
+
*
|
|
16
|
+
* @template {import('./message-port.js').LocalBindings} Local
|
|
17
|
+
* @template {import('./message-port.js').LocalBindings} Remote
|
|
18
|
+
* @param {MessagePort} port
|
|
19
|
+
* @param {Local} local Methods exposed to the remote side (sync or async).
|
|
20
|
+
* @returns {import('./message-port.js').RemoteProxy<Remote>}
|
|
11
21
|
*/
|
|
12
|
-
export default (port,
|
|
22
|
+
export default (port, local) => {
|
|
23
|
+
// secure the postMessage method to avoid potential
|
|
24
|
+
// MessagePort.prototype.postMessage overrides interfering
|
|
25
|
+
// with the communication channel
|
|
26
|
+
const post = port.postMessage.bind(port);
|
|
13
27
|
port.onmessage = async ({ data: [exec, id, name, args] }) => {
|
|
14
28
|
if (exec) {
|
|
15
|
-
try {
|
|
16
|
-
catch (error) {
|
|
29
|
+
try { post([!exec, id, await local[name](...args), null]) }
|
|
30
|
+
catch (error) { post([!exec, id, null, error]) }
|
|
17
31
|
}
|
|
18
32
|
else resolve(id, name, args);
|
|
19
33
|
};
|
|
20
|
-
return new Proxy(nil, {
|
|
34
|
+
return /** @type {import('./message-port.js').RemoteProxy<Remote>} */(new Proxy(nil, {
|
|
21
35
|
get(_, name) {
|
|
22
|
-
return (...args) => {
|
|
36
|
+
return (/** @type {any[]} */ ...args) => {
|
|
23
37
|
const [id, promise] = next();
|
|
24
|
-
|
|
38
|
+
post([true, id, name, args]);
|
|
25
39
|
return promise;
|
|
26
40
|
};
|
|
27
41
|
}
|
|
28
|
-
});
|
|
42
|
+
}));
|
|
29
43
|
};
|