@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 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
- const { port1, port2 } = new MessageChannel;
11
-
12
- // expose bindings the remote can invoke
13
- // the `remote` itself can invoke (asynchronously)
14
- // bindings exposed via the other port
15
- const remote = /** @type {{sum: (a:number, b:number) => Promise<number>}} */(
16
- bindings(port1, {
17
- log(...values) {
18
- console.log(...values);
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
- The remote counterpart can use or expose bindings in a similar way, this module just orchestrate a reliable way to invoke exposed APIs via `postMessage` dances.
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` to add bindings via *MessagePort* orchestration
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.1",
3
+ "version": "0.1.0",
4
4
  "description": "A (soon to be) collection of bindings for various scenarios.",
5
- "exports": {
6
- "./message-port": "./src/message-port.js"
5
+ "types": {
6
+ "./message-port": "./src/message-port.d.ts"
7
7
  },
8
- "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1"
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.0"
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;
@@ -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 that can be used to call methods remotely via the port.
8
- * @param {MessagePort} port - The port to use for communication.
9
- * @param {Record<string, (...args: any[]) => Promise<any>>} bindings - The bindings to use for the methods.
10
- * @returns {unknown}
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, bindings) => {
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 { port.postMessage([!exec, id, await bindings[name](...args), null]) }
16
- catch (error) { port.postMessage([!exec, id, null, 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
- port.postMessage([true, id, name, args]);
38
+ post([true, id, name, args]);
25
39
  return promise;
26
40
  };
27
41
  }
28
- });
42
+ }));
29
43
  };