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