1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 | const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort];
|
14 | const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
15 | const proxyValueSymbol = Symbol("proxyValue");
|
16 | const throwSymbol = Symbol("throw");
|
17 | const proxyTransferHandler = {
|
18 | canHandle: (obj) => obj && obj[proxyValueSymbol],
|
19 | serialize: (obj) => {
|
20 | const { port1, port2 } = new MessageChannel();
|
21 | expose(obj, port1);
|
22 | return port2;
|
23 | },
|
24 | deserialize: (obj) => {
|
25 | return proxy(obj);
|
26 | }
|
27 | };
|
28 | const throwTransferHandler = {
|
29 | canHandle: (obj) => obj && obj[throwSymbol],
|
30 | serialize: (obj) => obj.toString() + "\n" + obj.stack,
|
31 | deserialize: (obj) => {
|
32 | throw Error(obj);
|
33 | }
|
34 | };
|
35 | export const transferHandlers = new Map([
|
36 | ["PROXY", proxyTransferHandler],
|
37 | ["THROW", throwTransferHandler]
|
38 | ]);
|
39 | let pingPongMessageCounter = 0;
|
40 | export function proxy(endpoint, target) {
|
41 | if (isWindow(endpoint))
|
42 | endpoint = windowEndpoint(endpoint);
|
43 | if (!isEndpoint(endpoint))
|
44 | throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");
|
45 | activateEndpoint(endpoint);
|
46 | return cbProxy(async (irequest) => {
|
47 | let args = [];
|
48 | if (irequest.type === "APPLY" || irequest.type === "CONSTRUCT")
|
49 | args = irequest.argumentsList.map(wrapValue);
|
50 | const response = await pingPongMessage(endpoint, Object.assign({}, irequest, { argumentsList: args }), transferableProperties(args));
|
51 | const result = response.data;
|
52 | return unwrapValue(result.value);
|
53 | }, [], target);
|
54 | }
|
55 | export function proxyValue(obj) {
|
56 | obj[proxyValueSymbol] = true;
|
57 | return obj;
|
58 | }
|
59 | export function expose(rootObj, endpoint) {
|
60 | if (isWindow(endpoint))
|
61 | endpoint = windowEndpoint(endpoint);
|
62 | if (!isEndpoint(endpoint))
|
63 | throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");
|
64 | activateEndpoint(endpoint);
|
65 | attachMessageHandler(endpoint, async function (event) {
|
66 | if (!event.data.id || !event.data.callPath)
|
67 | return;
|
68 | const irequest = event.data;
|
69 | let that = await irequest.callPath
|
70 | .slice(0, -1)
|
71 | .reduce((obj, propName) => obj[propName], rootObj);
|
72 | let obj = await irequest.callPath.reduce((obj, propName) => obj[propName], rootObj);
|
73 | let iresult = obj;
|
74 | let args = [];
|
75 | if (irequest.type === "APPLY" || irequest.type === "CONSTRUCT")
|
76 | args = irequest.argumentsList.map(unwrapValue);
|
77 | if (irequest.type === "APPLY") {
|
78 | try {
|
79 | iresult = await obj.apply(that, args);
|
80 | }
|
81 | catch (e) {
|
82 | iresult = e;
|
83 | iresult[throwSymbol] = true;
|
84 | }
|
85 | }
|
86 | if (irequest.type === "CONSTRUCT") {
|
87 | try {
|
88 | iresult = new obj(...args);
|
89 | iresult = proxyValue(iresult);
|
90 | }
|
91 | catch (e) {
|
92 | iresult = e;
|
93 | iresult[throwSymbol] = true;
|
94 | }
|
95 | }
|
96 | if (irequest.type === "SET") {
|
97 | obj[irequest.property] = irequest.value;
|
98 |
|
99 |
|
100 | iresult = true;
|
101 | }
|
102 | iresult = makeInvocationResult(iresult);
|
103 | iresult.id = irequest.id;
|
104 | return endpoint.postMessage(iresult, transferableProperties([iresult]));
|
105 | });
|
106 | }
|
107 | function wrapValue(arg) {
|
108 |
|
109 | for (const [key, transferHandler] of transferHandlers) {
|
110 | if (transferHandler.canHandle(arg)) {
|
111 | return {
|
112 | type: key,
|
113 | value: transferHandler.serialize(arg)
|
114 | };
|
115 | }
|
116 | }
|
117 |
|
118 | let wrappedChildren = [];
|
119 | for (const item of iterateAllProperties(arg)) {
|
120 | for (const [key, transferHandler] of transferHandlers) {
|
121 | if (transferHandler.canHandle(item.value)) {
|
122 | wrappedChildren.push({
|
123 | path: item.path,
|
124 | wrappedValue: {
|
125 | type: key,
|
126 | value: transferHandler.serialize(item.value)
|
127 | }
|
128 | });
|
129 | }
|
130 | }
|
131 | }
|
132 | for (const wrappedChild of wrappedChildren) {
|
133 | const container = wrappedChild.path
|
134 | .slice(0, -1)
|
135 | .reduce((obj, key) => obj[key], arg);
|
136 | container[wrappedChild.path[wrappedChild.path.length - 1]] = null;
|
137 | }
|
138 | return {
|
139 | type: "RAW",
|
140 | value: arg,
|
141 | wrappedChildren
|
142 | };
|
143 | }
|
144 | function unwrapValue(arg) {
|
145 | if (transferHandlers.has(arg.type)) {
|
146 | const transferHandler = transferHandlers.get(arg.type);
|
147 | return transferHandler.deserialize(arg.value);
|
148 | }
|
149 | else if (isRawWrappedValue(arg)) {
|
150 | for (const wrappedChildValue of arg.wrappedChildren || []) {
|
151 | if (!transferHandlers.has(wrappedChildValue.wrappedValue.type))
|
152 | throw Error(`Unknown value type "${arg.type}" at ${wrappedChildValue.path.join(".")}`);
|
153 | const transferHandler = transferHandlers.get(wrappedChildValue.wrappedValue.type);
|
154 | const newValue = transferHandler.deserialize(wrappedChildValue.wrappedValue.value);
|
155 | replaceValueInObjectAtPath(arg.value, wrappedChildValue.path, newValue);
|
156 | }
|
157 | return arg.value;
|
158 | }
|
159 | else {
|
160 | throw Error(`Unknown value type "${arg.type}"`);
|
161 | }
|
162 | }
|
163 | function replaceValueInObjectAtPath(obj, path, newVal) {
|
164 | const lastKey = path.slice(-1)[0];
|
165 | const lastObj = path
|
166 | .slice(0, -1)
|
167 | .reduce((obj, key) => obj[key], obj);
|
168 | lastObj[lastKey] = newVal;
|
169 | }
|
170 | function isRawWrappedValue(arg) {
|
171 | return arg.type === "RAW";
|
172 | }
|
173 | function windowEndpoint(w) {
|
174 | if (self.constructor.name !== "Window")
|
175 | throw Error("self is not a window");
|
176 | return {
|
177 | addEventListener: self.addEventListener.bind(self),
|
178 | removeEventListener: self.removeEventListener.bind(self),
|
179 | postMessage: (msg, transfer) => w.postMessage(msg, "*", transfer)
|
180 | };
|
181 | }
|
182 | function isEndpoint(endpoint) {
|
183 | return ("addEventListener" in endpoint &&
|
184 | "removeEventListener" in endpoint &&
|
185 | "postMessage" in endpoint);
|
186 | }
|
187 | function activateEndpoint(endpoint) {
|
188 | if (isMessagePort(endpoint))
|
189 | endpoint.start();
|
190 | }
|
191 | function attachMessageHandler(endpoint, f) {
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 |
|
199 |
|
200 |
|
201 | endpoint.addEventListener("message", f);
|
202 | }
|
203 | function detachMessageHandler(endpoint, f) {
|
204 |
|
205 | endpoint.removeEventListener("message", f);
|
206 | }
|
207 | function isMessagePort(endpoint) {
|
208 | return endpoint.constructor.name === "MessagePort";
|
209 | }
|
210 | function isWindow(endpoint) {
|
211 |
|
212 |
|
213 | return ["window", "length", "location", "parent", "opener"].every(prop => prop in endpoint);
|
214 | }
|
215 |
|
216 |
|
217 |
|
218 |
|
219 | function pingPongMessage(endpoint, msg, transferables) {
|
220 | const id = `${uid}-${pingPongMessageCounter++}`;
|
221 | return new Promise(resolve => {
|
222 | attachMessageHandler(endpoint, function handler(event) {
|
223 | if (event.data.id !== id)
|
224 | return;
|
225 | detachMessageHandler(endpoint, handler);
|
226 | resolve(event);
|
227 | });
|
228 |
|
229 | msg = Object.assign({}, msg, { id });
|
230 | endpoint.postMessage(msg, transferables);
|
231 | });
|
232 | }
|
233 | function cbProxy(cb, callPath = [], target = function () { }) {
|
234 | return new Proxy(target, {
|
235 | construct(_target, argumentsList, proxy) {
|
236 | return cb({
|
237 | type: "CONSTRUCT",
|
238 | callPath,
|
239 | argumentsList
|
240 | });
|
241 | },
|
242 | apply(_target, _thisArg, argumentsList) {
|
243 |
|
244 |
|
245 | if (callPath[callPath.length - 1] === "bind")
|
246 | return cbProxy(cb, callPath.slice(0, -1));
|
247 | return cb({
|
248 | type: "APPLY",
|
249 | callPath,
|
250 | argumentsList
|
251 | });
|
252 | },
|
253 | get(_target, property, proxy) {
|
254 | if (property === "then" && callPath.length === 0) {
|
255 | return { then: () => proxy };
|
256 | }
|
257 | else if (property === "then") {
|
258 | const r = cb({
|
259 | type: "GET",
|
260 | callPath
|
261 | });
|
262 | return Promise.resolve(r).then.bind(r);
|
263 | }
|
264 | else {
|
265 | return cbProxy(cb, callPath.concat(property), _target[property]);
|
266 | }
|
267 | },
|
268 | set(_target, property, value, _proxy) {
|
269 | return cb({
|
270 | type: "SET",
|
271 | callPath,
|
272 | property,
|
273 | value
|
274 | });
|
275 | }
|
276 | });
|
277 | }
|
278 | function isTransferable(thing) {
|
279 | return TRANSFERABLE_TYPES.some(type => thing instanceof type);
|
280 | }
|
281 | function* iterateAllProperties(value, path = [], visited = null) {
|
282 | if (!value)
|
283 | return;
|
284 | if (!visited)
|
285 | visited = new WeakSet();
|
286 | if (visited.has(value))
|
287 | return;
|
288 | if (typeof value === "string")
|
289 | return;
|
290 | if (typeof value === "object")
|
291 | visited.add(value);
|
292 | if (ArrayBuffer.isView(value))
|
293 | return;
|
294 | yield { value, path };
|
295 | const keys = Object.keys(value);
|
296 | for (const key of keys)
|
297 | yield* iterateAllProperties(value[key], [...path, key], visited);
|
298 | }
|
299 | function transferableProperties(obj) {
|
300 | const r = [];
|
301 | for (const prop of iterateAllProperties(obj)) {
|
302 | if (isTransferable(prop.value))
|
303 | r.push(prop.value);
|
304 | }
|
305 | return r;
|
306 | }
|
307 | function makeInvocationResult(obj) {
|
308 | for (const [type, transferHandler] of transferHandlers) {
|
309 | if (transferHandler.canHandle(obj)) {
|
310 | const value = transferHandler.serialize(obj);
|
311 | return {
|
312 | value: { type, value }
|
313 | };
|
314 | }
|
315 | }
|
316 | return {
|
317 | value: {
|
318 | type: "RAW",
|
319 | value: obj
|
320 | }
|
321 | };
|
322 | }
|