UNPKG

10 kBPlain TextView Raw
1/**
2 * Copyright 2019 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
14import {
15 Endpoint,
16 EventSource,
17 Message,
18 MessageType,
19 PostMessageWithOrigin,
20 WireValue,
21 WireValueType
22} from "./protocol.js";
23export { Endpoint };
24
25export const proxyMarker = Symbol("Comlink.proxy");
26export const createEndpoint = Symbol("Comlink.endpoint");
27export const releaseProxy = Symbol("Comlink.releaseProxy");
28const throwSet = new WeakSet();
29
30// prettier-ignore
31type Promisify<T> = T extends { [proxyMarker]: boolean }
32 ? Promise<Remote<T>>
33 : T extends Promise<any>
34 ? T
35 : T extends (...args: infer R1) => infer R2
36 ? (...args: R1) => Promisify<R2>
37 : Promise<T>;
38
39// prettier-ignore
40export type Remote<T> =
41 (
42 T extends (...args: infer R1) => infer R2
43 ? (...args: R1) => Promisify<R2>
44 : { [K in keyof T]: Promisify<T[K]> }
45 ) & (
46 T extends { new (...args: infer R1): infer R2 }
47 ? { new (...args: R1): Promise<Remote<R2>> }
48 : unknown
49 );
50
51export interface TransferHandler {
52 canHandle(obj: any): boolean;
53 serialize(obj: any): [any, Transferable[]];
54 deserialize(obj: any): any;
55}
56
57export const transferHandlers = new Map<string, TransferHandler>([
58 [
59 "proxy",
60 {
61 canHandle: obj => obj && obj[proxyMarker],
62 serialize(obj) {
63 const { port1, port2 } = new MessageChannel();
64 expose(obj, port1);
65 return [port2, [port2]];
66 },
67 deserialize: (port: MessagePort) => {
68 port.start();
69 return wrap(port);
70 }
71 }
72 ],
73 [
74 "throw",
75 {
76 canHandle: obj => throwSet.has(obj),
77 serialize(obj) {
78 const isError = obj instanceof Error;
79 let serialized = obj;
80 if (isError) {
81 serialized = {
82 isError,
83 message: obj.message,
84 stack: obj.stack
85 };
86 }
87 return [serialized, []];
88 },
89 deserialize(obj) {
90 if ((obj as any).isError) {
91 throw Object.assign(new Error(), obj);
92 }
93 throw obj;
94 }
95 }
96 ]
97]);
98
99export function expose(obj: any, ep: Endpoint = self as any) {
100 ep.addEventListener("message", async function callback(ev: MessageEvent) {
101 if (!ev || !ev.data) {
102 return;
103 }
104 const { id, type, path } = {
105 path: [] as string[],
106 ...(ev.data as Message)
107 };
108 const argumentList = (ev.data.argumentList || []).map(fromWireValue);
109 let returnValue;
110 try {
111 const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);
112 const rawValue = path.reduce((obj, prop) => obj[prop], obj);
113 switch (type) {
114 case MessageType.GET:
115 {
116 returnValue = await rawValue;
117 }
118 break;
119 case MessageType.SET:
120 {
121 parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);
122 returnValue = true;
123 }
124 break;
125 case MessageType.APPLY:
126 {
127 returnValue = await rawValue.apply(parent, argumentList);
128 }
129 break;
130 case MessageType.CONSTRUCT:
131 {
132 const value = await new rawValue(...argumentList);
133 returnValue = proxy(value);
134 }
135 break;
136 case MessageType.ENDPOINT:
137 {
138 const { port1, port2 } = new MessageChannel();
139 expose(obj, port2);
140 returnValue = transfer(port1, [port1]);
141 }
142 break;
143 case MessageType.RELEASE:
144 {
145 returnValue = undefined;
146 }
147 break;
148 }
149 } catch (e) {
150 returnValue = e;
151 throwSet.add(e);
152 }
153 const [wireValue, transferables] = toWireValue(returnValue);
154 ep.postMessage({ ...wireValue, id }, transferables);
155 if (type === MessageType.RELEASE) {
156 // detach and deactive after sending release response above.
157 ep.removeEventListener("message", callback as any);
158 closeEndPoint(ep);
159 }
160 } as any);
161 if (ep.start) {
162 ep.start();
163 }
164}
165
166function isMessagePort(endpoint: Endpoint): endpoint is MessagePort {
167 return endpoint.constructor.name === "MessagePort";
168}
169
170function closeEndPoint(endpoint: Endpoint) {
171 if (isMessagePort(endpoint)) endpoint.close();
172}
173
174export function wrap<T>(ep: Endpoint): Remote<T> {
175 return createProxy<T>(ep) as any;
176}
177
178function throwIfProxyReleased(isReleased: boolean) {
179 if (isReleased) {
180 throw new Error("Proxy has been released and is not useable");
181 }
182}
183
184function createProxy<T>(
185 ep: Endpoint,
186 path: (string | number | symbol)[] = []
187): Remote<T> {
188 let isProxyReleased = false;
189 const proxy = new Proxy(function() {}, {
190 get(_target, prop) {
191 throwIfProxyReleased(isProxyReleased);
192 if (prop === releaseProxy) {
193 return () => {
194 return requestResponseMessage(ep, {
195 type: MessageType.RELEASE,
196 path: path.map(p => p.toString())
197 }).then(() => {
198 closeEndPoint(ep);
199 isProxyReleased = true;
200 });
201 };
202 }
203 if (prop === "then") {
204 if (path.length === 0) {
205 return { then: () => proxy };
206 }
207 const r = requestResponseMessage(ep, {
208 type: MessageType.GET,
209 path: path.map(p => p.toString())
210 }).then(fromWireValue);
211 return r.then.bind(r);
212 }
213 return createProxy(ep, [...path, prop]);
214 },
215 set(_target, prop, rawValue) {
216 throwIfProxyReleased(isProxyReleased);
217 // FIXME: ES6 Proxy Handler `set` methods are supposed to return a
218 // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯
219 const [value, transferables] = toWireValue(rawValue);
220 return requestResponseMessage(
221 ep,
222 {
223 type: MessageType.SET,
224 path: [...path, prop].map(p => p.toString()),
225 value
226 },
227 transferables
228 ).then(fromWireValue) as any;
229 },
230 apply(_target, _thisArg, rawArgumentList) {
231 throwIfProxyReleased(isProxyReleased);
232 const last = path[path.length - 1];
233 if ((last as any) === createEndpoint) {
234 return requestResponseMessage(ep, {
235 type: MessageType.ENDPOINT
236 }).then(fromWireValue);
237 }
238 // We just pretend that `bind()` didn’t happen.
239 if (last === "bind") {
240 return createProxy(ep, path.slice(0, -1));
241 }
242 const [argumentList, transferables] = processArguments(rawArgumentList);
243 return requestResponseMessage(
244 ep,
245 {
246 type: MessageType.APPLY,
247 path: path.map(p => p.toString()),
248 argumentList
249 },
250 transferables
251 ).then(fromWireValue);
252 },
253 construct(_target, rawArgumentList) {
254 throwIfProxyReleased(isProxyReleased);
255 const [argumentList, transferables] = processArguments(rawArgumentList);
256 return requestResponseMessage(
257 ep,
258 {
259 type: MessageType.CONSTRUCT,
260 path: path.map(p => p.toString()),
261 argumentList
262 },
263 transferables
264 ).then(fromWireValue);
265 }
266 });
267 return proxy as any;
268}
269
270function myFlat<T>(arr: (T | T[])[]): T[] {
271 return Array.prototype.concat.apply([], arr);
272}
273
274function processArguments(argumentList: any[]): [WireValue[], Transferable[]] {
275 const processed = argumentList.map(toWireValue);
276 return [processed.map(v => v[0]), myFlat(processed.map(v => v[1]))];
277}
278
279const transferCache = new WeakMap<any, Transferable[]>();
280export function transfer(obj: any, transfers: Transferable[]) {
281 transferCache.set(obj, transfers);
282 return obj;
283}
284
285export function proxy<T>(obj: T): T & { [proxyMarker]: true } {
286 return Object.assign(obj, { [proxyMarker]: true }) as any;
287}
288
289export function windowEndpoint(
290 w: PostMessageWithOrigin,
291 context: EventSource = self
292): Endpoint {
293 return {
294 postMessage: (msg: any, transferables: Transferable[]) =>
295 w.postMessage(msg, "*", transferables),
296 addEventListener: context.addEventListener.bind(context),
297 removeEventListener: context.removeEventListener.bind(context)
298 };
299}
300
301function toWireValue(value: any): [WireValue, Transferable[]] {
302 for (const [name, handler] of transferHandlers) {
303 if (handler.canHandle(value)) {
304 const [serializedValue, transferables] = handler.serialize(value);
305 return [
306 {
307 type: WireValueType.HANDLER,
308 name,
309 value: serializedValue
310 },
311 transferables
312 ];
313 }
314 }
315 return [
316 {
317 type: WireValueType.RAW,
318 value
319 },
320 transferCache.get(value) || []
321 ];
322}
323
324function fromWireValue(value: WireValue): any {
325 switch (value.type) {
326 case WireValueType.HANDLER:
327 return transferHandlers.get(value.name)!.deserialize(value.value);
328 case WireValueType.RAW:
329 return value.value;
330 }
331}
332
333function requestResponseMessage(
334 ep: Endpoint,
335 msg: Message,
336 transfers?: Transferable[]
337): Promise<WireValue> {
338 return new Promise(resolve => {
339 const id = generateUUID();
340 ep.addEventListener("message", function l(ev: MessageEvent) {
341 if (!ev.data || !ev.data.id || ev.data.id !== id) {
342 return;
343 }
344 ep.removeEventListener("message", l as any);
345 resolve(ev.data);
346 } as any);
347 if (ep.start) {
348 ep.start();
349 }
350 ep.postMessage({ id, ...msg }, transfers);
351 });
352}
353
354function generateUUID(): string {
355 return new Array(4)
356 .fill(0)
357 .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))
358 .join("-");
359}