@webreflection/bindings 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @webreflection/bindings
2
2
 
3
+ [![Coverage Status](https://coveralls.io/repos/github/WebReflection/bindings/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/bindings?branch=main)
4
+
3
5
  A (soon to be) collection of bindings for various scenarios.
4
6
 
5
7
  ### Example
@@ -64,6 +66,71 @@ await worker.sum(1, 2);
64
66
 
65
67
  Remote methods always return a `Promise`, even when the handler on the other side is synchronous.
66
68
 
69
+ #### Transferable responses
70
+
71
+ Import `options` and assign `this[options]` inside a local handler to pass
72
+ `postMessage` options (typically `{ transfer: [...] }`) with the response.
73
+ `options` is the only symbol used by this module. `bindings()` installs a
74
+ setter-only accessor on the `local` object — assign before returning; it
75
+ cannot be read back. Use method syntax (not arrow functions) so `this` refers
76
+ to `local`:
77
+
78
+ ```js
79
+ import bindings, { options } from '@webreflection/bindings/message-port';
80
+
81
+ /** @typedef {import('@webreflection/bindings/message-port').RemoteProxy} RemoteProxy */
82
+ /** @typedef {import('@webreflection/bindings/message-port').LocalOptionsHost} LocalOptionsHost */
83
+
84
+ const { port1, port2 } = new MessageChannel();
85
+
86
+ // this side exposes `readBuffer` to the other side
87
+ bindings(port1, {
88
+ /** @this {LocalOptionsHost} */
89
+ readBuffer() {
90
+ const i32a = new Int32Array([1, 2, 3]);
91
+ this[options] = { transfer: [i32a.buffer] };
92
+ return i32a;
93
+ },
94
+ });
95
+
96
+ // other side calls it through the returned proxy
97
+ /** @type {RemoteProxy<{ readBuffer: () => Int32Array }>} */
98
+ const remote = bindings(port2, {});
99
+
100
+ const i32a = await remote.readBuffer();
101
+ ```
102
+
103
+ ```ts
104
+ import bindings, {
105
+ options,
106
+ type LocalOptionsHost,
107
+ type RemoteProxy,
108
+ } from '@webreflection/bindings/message-port';
109
+
110
+ const { port1, port2 } = new MessageChannel();
111
+
112
+ type WorkerAPI = { readBuffer: () => Int32Array<ArrayBuffer> };
113
+
114
+ bindings(port1, {
115
+ readBuffer(this: LocalOptionsHost) {
116
+ const i32a = new Int32Array([1, 2, 3]);
117
+ this[options] = { transfer: [i32a.buffer] };
118
+ return i32a;
119
+ },
120
+ });
121
+
122
+ const worker = bindings<Record<string, never>, WorkerAPI>(port2, {});
123
+ await worker.readBuffer();
124
+ ```
125
+
126
+ There is a single `opts` slot per `bindings()` call. Treat `this[options]` as an
127
+ advanced feature: assign it right before returning, when something must be
128
+ transferred, and only if you understand concurrent handlers on the same port
129
+ can overwrite each other's options while they are in flight.
130
+
131
+ On failure, any options set during that handler are discarded (`finally` clears
132
+ the slot) and the error reply is sent without them.
133
+
67
134
  ## Currently Available
68
135
 
69
136
  * `@webreflection/bindings/message-port` — bind APIs over a *MessagePort* (workers, iframes, `MessageChannel`, etc.)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webreflection/bindings",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "A (soon to be) collection of bindings for various scenarios.",
5
5
  "types": {
6
6
  "./message-port": "./src/message-port.d.ts"
@@ -15,13 +15,27 @@
15
15
  "bindings",
16
16
  "MessagePort"
17
17
  ],
18
+ "files": [
19
+ "src",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
18
23
  "author": "Andrea Giammarchi",
19
24
  "license": "MIT",
20
25
  "type": "module",
26
+ "scripts": {
27
+ "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info",
28
+ "test": "c8 node test/verify-runtime.mjs"
29
+ },
21
30
  "dependencies": {
22
31
  "@webreflection/utils": "^0.3.6",
23
32
  "next-resolver": "^0.2.2"
24
33
  },
34
+ "overrides": {
35
+ "c8": {
36
+ "yargs": "^18.0.0"
37
+ }
38
+ },
25
39
  "main": "index.js",
26
40
  "repository": {
27
41
  "type": "git",
@@ -30,5 +44,8 @@
30
44
  "bugs": {
31
45
  "url": "https://github.com/WebReflection/bindings/issues"
32
46
  },
33
- "homepage": "https://github.com/WebReflection/bindings#readme"
47
+ "homepage": "https://github.com/WebReflection/bindings#readme",
48
+ "devDependencies": {
49
+ "c8": "^11.0.0"
50
+ }
34
51
  }
@@ -1,3 +1,25 @@
1
+ /** `postMessage` options for the next binding response (`transfer`, etc.). */
2
+ export type PostMessageOptions = StructuredSerializeOptions;
3
+
4
+ /**
5
+ * The only symbol exported by this module.
6
+ *
7
+ * {@link bindings} installs a setter-only `this[options]` accessor on the
8
+ * `local` object. Assign {@link PostMessageOptions} from local handlers;
9
+ * the value is not readable, is consumed on the next response, then cleared
10
+ * (including after errors).
11
+ */
12
+ export declare const options: unique symbol;
13
+
14
+ /**
15
+ * `local` object passed to {@link bindings}, after which handlers can assign
16
+ * `this[options]`. TypeScript has no write-only property type — reading
17
+ * `this[options]` is unsupported at runtime.
18
+ */
19
+ export type LocalOptionsHost = {
20
+ [options]: PostMessageOptions;
21
+ };
22
+
1
23
  /** Any function exposed as a local binding (sync or async). */
2
24
  export type Handler = (...args: any[]) => any;
3
25
 
@@ -1,10 +1,29 @@
1
1
  //@ts-check
2
2
 
3
3
  import nextResolver from 'next-resolver';
4
+ import Map from '@webreflection/utils/map';
4
5
  import { nil } from '@webreflection/utils/empty';
5
6
 
7
+ const { defineProperty } = Object;
6
8
  const [next, resolve] = nextResolver();
7
9
 
10
+ /**
11
+ * @typedef {StructuredSerializeOptions} PostMessageOptions
12
+ */
13
+
14
+ /**
15
+ * The only symbol exported by this module.
16
+ *
17
+ * {@link bindings} installs a setter-only `this[options]` accessor on the
18
+ * `local` object. Assign {@link PostMessageOptions} from local handlers
19
+ * before returning (e.g. `{ transfer: [...] }`). The value cannot be read
20
+ * back; it is consumed with the next response `postMessage`, then cleared
21
+ * (including after errors).
22
+ *
23
+ * @type {unique symbol}
24
+ */
25
+ export const options = Symbol();
26
+
8
27
  /**
9
28
  * Creates a proxy for calling methods exposed on the remote side of the port,
10
29
  * while dispatching incoming calls to `local` on this side.
@@ -13,31 +32,47 @@ const [next, resolve] = nextResolver();
13
32
  * from that object alone: pass a second generic or assert the return type
14
33
  * (see README).
15
34
  *
35
+ * Also installs a setter-only `this[options]` accessor on `local` so handlers
36
+ * can assign {@link PostMessageOptions} for their response.
37
+ *
16
38
  * @template {import('./message-port.js').LocalBindings} Local
17
39
  * @template {import('./message-port.js').LocalBindings} Remote
18
40
  * @param {MessagePort} port
19
41
  * @param {Local} local Methods exposed to the remote side (sync or async).
20
42
  * @returns {import('./message-port.js').RemoteProxy<Remote>}
21
43
  */
22
- export default (port, local) => {
44
+ const bindings = (port, local) => {
23
45
  // secure the postMessage method to avoid potential
24
46
  // MessagePort.prototype.postMessage overrides interfering
25
47
  // with the communication channel
26
- const post = port.postMessage.bind(port);
48
+ const post = port.postMessage.bind(port), $ = new Map;
49
+
50
+ let opts = nil;
51
+
52
+ defineProperty(local, options, {
53
+ configurable: false,
54
+ set(value) { opts = value },
55
+ });
56
+
27
57
  port.onmessage = async ({ data: [exec, id, name, args] }) => {
28
58
  if (exec) {
29
- try { post([!exec, id, await local[name](...args), null]) }
59
+ try {
60
+ const result = await local[name](...args);
61
+ post([!exec, id, result, null], opts);
62
+ }
30
63
  catch (error) { post([!exec, id, null, error]) }
64
+ finally { opts = nil }
31
65
  }
32
66
  else resolve(id, name, args);
33
67
  };
68
+
34
69
  return /** @type {import('./message-port.js').RemoteProxy<Remote>} */(new Proxy(nil, {
35
- get(_, name) {
36
- return (/** @type {any[]} */ ...args) => {
37
- const [id, promise] = next();
38
- post([true, id, name, args]);
39
- return promise;
40
- };
41
- }
70
+ get: (_, name) => $.get(name) ?? $.put(name, (/** @type {any[]} */ ...args) => {
71
+ const [id, promise] = next();
72
+ post([true, id, name, args]);
73
+ return promise;
74
+ }),
42
75
  }));
43
76
  };
77
+
78
+ export default bindings;