UNPKG

13.9 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 */
13(function (factory) {
14 if (typeof module === "object" && typeof module.exports === "object") {
15 var v = factory(require, exports);
16 if (v !== undefined) module.exports = v;
17 }
18 else if (typeof define === "function" && define.amd) {
19 define(["require", "exports"], factory);
20 }
21else {factory([], self.Comlink={});}
22})(function (require, exports) {
23 "use strict";
24 Object.defineProperty(exports, "__esModule", { value: true });
25 /**
26 * Symbol that gets added to objects by `Comlink.proxy()`.
27 */
28 exports.proxyValueSymbol = Symbol("comlinkProxyValue");
29 /**
30 * Returns true if the given value has the proxy value symbol added to it.
31 */
32 const isProxyValue = (value) => !!value && value[exports.proxyValueSymbol] === true;
33 const TRANSFERABLE_TYPES = ["ArrayBuffer", "MessagePort", "OffscreenCanvas"]
34 .filter(f => f in self)
35 .map(f => self[f]);
36 const uid = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
37 const throwSymbol = Symbol("throw");
38 const proxyTransferHandler = {
39 canHandle: isProxyValue,
40 serialize: (obj) => {
41 const { port1, port2 } = new MessageChannel();
42 expose(obj, port1);
43 return port2;
44 },
45 deserialize: (obj) => {
46 return proxy(obj);
47 }
48 };
49 const throwTransferHandler = {
50 canHandle: (obj) => obj && obj[throwSymbol],
51 serialize: (obj) => {
52 const message = obj && obj.message;
53 const stack = obj && obj.stack;
54 return Object.assign({}, obj, { message, stack });
55 },
56 deserialize: (obj) => {
57 throw Object.assign(Error(), obj);
58 }
59 };
60 exports.transferHandlers = new Map([
61 ["PROXY", proxyTransferHandler],
62 ["THROW", throwTransferHandler]
63 ]);
64 let pingPongMessageCounter = 0;
65 function proxy(endpoint, target) {
66 if (isWindow(endpoint))
67 endpoint = windowEndpoint(endpoint);
68 if (!isEndpoint(endpoint))
69 throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");
70 activateEndpoint(endpoint);
71 return cbProxy(async (irequest) => {
72 let args = [];
73 if (irequest.type === "APPLY" || irequest.type === "CONSTRUCT")
74 args = irequest.argumentsList.map(wrapValue);
75 const response = await pingPongMessage(endpoint, Object.assign({}, irequest, { argumentsList: args }), transferableProperties(args));
76 const result = response.data;
77 return unwrapValue(result.value);
78 }, [], target);
79 }
80 exports.proxy = proxy;
81 function proxyValue(obj) {
82 const proxyVal = obj;
83 proxyVal[exports.proxyValueSymbol] = true;
84 return proxyVal;
85 }
86 exports.proxyValue = proxyValue;
87 function expose(rootObj, endpoint) {
88 if (isWindow(endpoint))
89 endpoint = windowEndpoint(endpoint);
90 if (!isEndpoint(endpoint))
91 throw Error("endpoint does not have all of addEventListener, removeEventListener and postMessage defined");
92 activateEndpoint(endpoint);
93 attachMessageHandler(endpoint, async function (event) {
94 if (!event.data.id || !event.data.callPath)
95 return;
96 const irequest = event.data;
97 let that = await irequest.callPath
98 .slice(0, -1)
99 .reduce((obj, propName) => obj[propName], rootObj);
100 let obj = await irequest.callPath.reduce((obj, propName) => obj[propName], rootObj);
101 let iresult = obj;
102 let args = [];
103 if (irequest.type === "APPLY" || irequest.type === "CONSTRUCT")
104 args = irequest.argumentsList.map(unwrapValue);
105 if (irequest.type === "APPLY") {
106 try {
107 iresult = await obj.apply(that, args);
108 }
109 catch (e) {
110 iresult = e;
111 iresult[throwSymbol] = true;
112 }
113 }
114 if (irequest.type === "CONSTRUCT") {
115 try {
116 iresult = new obj(...args); // eslint-disable-line new-cap
117 iresult = proxyValue(iresult);
118 }
119 catch (e) {
120 iresult = e;
121 iresult[throwSymbol] = true;
122 }
123 }
124 if (irequest.type === "SET") {
125 obj[irequest.property] = irequest.value;
126 // FIXME: ES6 Proxy Handler `set` methods are supposed to return a
127 // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯
128 iresult = true;
129 }
130 iresult = makeInvocationResult(iresult);
131 iresult.id = irequest.id;
132 return endpoint.postMessage(iresult, transferableProperties([iresult]));
133 });
134 }
135 exports.expose = expose;
136 function wrapValue(arg) {
137 // Is arg itself handled by a TransferHandler?
138 for (const [key, transferHandler] of exports.transferHandlers) {
139 if (transferHandler.canHandle(arg)) {
140 return {
141 type: key,
142 value: transferHandler.serialize(arg)
143 };
144 }
145 }
146 // If not, traverse the entire object and find handled values.
147 let wrappedChildren = [];
148 for (const item of iterateAllProperties(arg)) {
149 for (const [key, transferHandler] of exports.transferHandlers) {
150 if (transferHandler.canHandle(item.value)) {
151 wrappedChildren.push({
152 path: item.path,
153 wrappedValue: {
154 type: key,
155 value: transferHandler.serialize(item.value)
156 }
157 });
158 }
159 }
160 }
161 for (const wrappedChild of wrappedChildren) {
162 const container = wrappedChild.path
163 .slice(0, -1)
164 .reduce((obj, key) => obj[key], arg);
165 container[wrappedChild.path[wrappedChild.path.length - 1]] = null;
166 }
167 return {
168 type: "RAW",
169 value: arg,
170 wrappedChildren
171 };
172 }
173 function unwrapValue(arg) {
174 if (exports.transferHandlers.has(arg.type)) {
175 const transferHandler = exports.transferHandlers.get(arg.type);
176 return transferHandler.deserialize(arg.value);
177 }
178 else if (isRawWrappedValue(arg)) {
179 for (const wrappedChildValue of arg.wrappedChildren || []) {
180 if (!exports.transferHandlers.has(wrappedChildValue.wrappedValue.type))
181 throw Error(`Unknown value type "${arg.type}" at ${wrappedChildValue.path.join(".")}`);
182 const transferHandler = exports.transferHandlers.get(wrappedChildValue.wrappedValue.type);
183 const newValue = transferHandler.deserialize(wrappedChildValue.wrappedValue.value);
184 replaceValueInObjectAtPath(arg.value, wrappedChildValue.path, newValue);
185 }
186 return arg.value;
187 }
188 else {
189 throw Error(`Unknown value type "${arg.type}"`);
190 }
191 }
192 function replaceValueInObjectAtPath(obj, path, newVal) {
193 const lastKey = path.slice(-1)[0];
194 const lastObj = path
195 .slice(0, -1)
196 .reduce((obj, key) => obj[key], obj);
197 lastObj[lastKey] = newVal;
198 }
199 function isRawWrappedValue(arg) {
200 return arg.type === "RAW";
201 }
202 function windowEndpoint(w) {
203 if (self.constructor.name !== "Window")
204 throw Error("self is not a window");
205 return {
206 addEventListener: self.addEventListener.bind(self),
207 removeEventListener: self.removeEventListener.bind(self),
208 postMessage: (msg, transfer) => w.postMessage(msg, "*", transfer)
209 };
210 }
211 function isEndpoint(endpoint) {
212 return ("addEventListener" in endpoint &&
213 "removeEventListener" in endpoint &&
214 "postMessage" in endpoint);
215 }
216 function activateEndpoint(endpoint) {
217 if (isMessagePort(endpoint))
218 endpoint.start();
219 }
220 function attachMessageHandler(endpoint, f) {
221 // Checking all possible types of `endpoint` manually satisfies TypeScript’s
222 // type checker. Not sure why the inference is failing here. Since it’s
223 // unnecessary code I’m going to resort to `any` for now.
224 // if(isWorker(endpoint))
225 // endpoint.addEventListener('message', f);
226 // if(isMessagePort(endpoint))
227 // endpoint.addEventListener('message', f);
228 // if(isOtherWindow(endpoint))
229 // endpoint.addEventListener('message', f);
230 endpoint.addEventListener("message", f);
231 }
232 function detachMessageHandler(endpoint, f) {
233 // Same as above.
234 endpoint.removeEventListener("message", f);
235 }
236 function isMessagePort(endpoint) {
237 return endpoint.constructor.name === "MessagePort";
238 }
239 function isWindow(endpoint) {
240 // TODO: This doesn’t work on cross-origin iframes.
241 // return endpoint.constructor.name === 'Window';
242 return ["window", "length", "location", "parent", "opener"].every(prop => prop in endpoint);
243 }
244 /**
245 * `pingPongMessage` sends a `postMessage` and waits for a reply. Replies are
246 * identified by a unique id that is attached to the payload.
247 */
248 function pingPongMessage(endpoint, msg, transferables) {
249 const id = `${uid}-${pingPongMessageCounter++}`;
250 return new Promise(resolve => {
251 attachMessageHandler(endpoint, function handler(event) {
252 if (event.data.id !== id)
253 return;
254 detachMessageHandler(endpoint, handler);
255 resolve(event);
256 });
257 // Copy msg and add `id` property
258 msg = Object.assign({}, msg, { id });
259 endpoint.postMessage(msg, transferables);
260 });
261 }
262 function cbProxy(cb, callPath = [], target = function () { }) {
263 return new Proxy(target, {
264 construct(_target, argumentsList, proxy) {
265 return cb({
266 type: "CONSTRUCT",
267 callPath,
268 argumentsList
269 });
270 },
271 apply(_target, _thisArg, argumentsList) {
272 // We use `bind` as an indicator to have a remote function bound locally.
273 // The actual target for `bind()` is currently ignored.
274 if (callPath[callPath.length - 1] === "bind")
275 return cbProxy(cb, callPath.slice(0, -1));
276 return cb({
277 type: "APPLY",
278 callPath,
279 argumentsList
280 });
281 },
282 get(_target, property, proxy) {
283 if (property === "then" && callPath.length === 0) {
284 return { then: () => proxy };
285 }
286 else if (property === "then") {
287 const r = cb({
288 type: "GET",
289 callPath
290 });
291 return Promise.resolve(r).then.bind(r);
292 }
293 else {
294 return cbProxy(cb, callPath.concat(property), _target[property]);
295 }
296 },
297 set(_target, property, value, _proxy) {
298 return cb({
299 type: "SET",
300 callPath,
301 property,
302 value
303 });
304 }
305 });
306 }
307 function isTransferable(thing) {
308 return TRANSFERABLE_TYPES.some(type => thing instanceof type);
309 }
310 function* iterateAllProperties(value, path = [], visited = null) {
311 if (!value)
312 return;
313 if (!visited)
314 visited = new WeakSet();
315 if (visited.has(value))
316 return;
317 if (typeof value === "string")
318 return;
319 if (typeof value === "object")
320 visited.add(value);
321 if (ArrayBuffer.isView(value))
322 return;
323 yield { value, path };
324 const keys = Object.keys(value);
325 for (const key of keys)
326 yield* iterateAllProperties(value[key], [...path, key], visited);
327 }
328 function transferableProperties(obj) {
329 const r = [];
330 for (const prop of iterateAllProperties(obj)) {
331 if (isTransferable(prop.value))
332 r.push(prop.value);
333 }
334 return r;
335 }
336 function makeInvocationResult(obj) {
337 for (const [type, transferHandler] of exports.transferHandlers) {
338 if (transferHandler.canHandle(obj)) {
339 const value = transferHandler.serialize(obj);
340 return {
341 value: { type, value }
342 };
343 }
344 }
345 return {
346 value: {
347 type: "RAW",
348 value: obj
349 }
350 };
351 }
352});