UNPKG

11.5 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All Rights Reserved.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 * http://www.apache.org/licenses/LICENSE-2.0
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS,
9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 * See the License for the specific language governing permissions and
11 * limitations under the License.
12 */
13const TRANSFERABLE_TYPES = [ArrayBuffer, MessagePort];
14const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
15const proxyValueSymbol = Symbol("proxyValue");
16const throwSymbol = Symbol("throw");
17const 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};
28const throwTransferHandler = {
29 canHandle: (obj) => obj && obj[throwSymbol],
30 serialize: (obj) => obj.toString() + "\n" + obj.stack,
31 deserialize: (obj) => {
32 throw Error(obj);
33 }
34};
35export const transferHandlers = new Map([
36 ["PROXY", proxyTransferHandler],
37 ["THROW", throwTransferHandler]
38]);
39let pingPongMessageCounter = 0;
40export 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}
55export function proxyValue(obj) {
56 obj[proxyValueSymbol] = true;
57 return obj;
58}
59export 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); // eslint-disable-line new-cap
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 // FIXME: ES6 Proxy Handler `set` methods are supposed to return a
99 // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯
100 iresult = true;
101 }
102 iresult = makeInvocationResult(iresult);
103 iresult.id = irequest.id;
104 return endpoint.postMessage(iresult, transferableProperties([iresult]));
105 });
106}
107function wrapValue(arg) {
108 // Is arg itself handled by a TransferHandler?
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 // If not, traverse the entire object and find handled values.
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}
144function 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}
163function 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}
170function isRawWrappedValue(arg) {
171 return arg.type === "RAW";
172}
173function 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}
182function isEndpoint(endpoint) {
183 return ("addEventListener" in endpoint &&
184 "removeEventListener" in endpoint &&
185 "postMessage" in endpoint);
186}
187function activateEndpoint(endpoint) {
188 if (isMessagePort(endpoint))
189 endpoint.start();
190}
191function attachMessageHandler(endpoint, f) {
192 // Checking all possible types of `endpoint` manually satisfies TypeScript’s
193 // type checker. Not sure why the inference is failing here. Since it’s
194 // unnecessary code I’m going to resort to `any` for now.
195 // if(isWorker(endpoint))
196 // endpoint.addEventListener('message', f);
197 // if(isMessagePort(endpoint))
198 // endpoint.addEventListener('message', f);
199 // if(isOtherWindow(endpoint))
200 // endpoint.addEventListener('message', f);
201 endpoint.addEventListener("message", f);
202}
203function detachMessageHandler(endpoint, f) {
204 // Same as above.
205 endpoint.removeEventListener("message", f);
206}
207function isMessagePort(endpoint) {
208 return endpoint.constructor.name === "MessagePort";
209}
210function isWindow(endpoint) {
211 // TODO: This doesn’t work on cross-origin iframes.
212 // return endpoint.constructor.name === 'Window';
213 return ["window", "length", "location", "parent", "opener"].every(prop => prop in endpoint);
214}
215/**
216 * `pingPongMessage` sends a `postMessage` and waits for a reply. Replies are
217 * identified by a unique id that is attached to the payload.
218 */
219function 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 // Copy msg and add `id` property
229 msg = Object.assign({}, msg, { id });
230 endpoint.postMessage(msg, transferables);
231 });
232}
233function 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 // We use `bind` as an indicator to have a remote function bound locally.
244 // The actual target for `bind()` is currently ignored.
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}
278function isTransferable(thing) {
279 return TRANSFERABLE_TYPES.some(type => thing instanceof type);
280}
281function* 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}
299function 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}
307function 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}