@webreflection/bindings 0.1.0 → 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 +65 -0
- package/package.json +10 -2
- package/src/message-port.d.ts +22 -0
- package/src/message-port.js +38 -2
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.
|
|
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,12 +15,20 @@
|
|
|
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
|
-
"next-resolver": "^0.2.
|
|
31
|
+
"next-resolver": "^0.2.2"
|
|
24
32
|
},
|
|
25
33
|
"main": "index.js",
|
|
26
34
|
"repository": {
|
package/src/message-port.d.ts
CHANGED
|
@@ -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
|
|
package/src/message-port.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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;
|