UNPKG

18.8 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2019 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import {
8 Endpoint,
9 EventSource,
10 Message,
11 MessageType,
12 PostMessageWithOrigin,
13 WireValue,
14 WireValueType,
15} from "./protocol";
16export type { Endpoint };
17
18export const proxyMarker = Symbol("Comlink.proxy");
19export const createEndpoint = Symbol("Comlink.endpoint");
20export const releaseProxy = Symbol("Comlink.releaseProxy");
21export const finalizer = Symbol("Comlink.finalizer");
22
23const throwMarker = Symbol("Comlink.thrown");
24
25/**
26 * Interface of values that were marked to be proxied with `comlink.proxy()`.
27 * Can also be implemented by classes.
28 */
29export interface ProxyMarked {
30 [proxyMarker]: true;
31}
32
33/**
34 * Takes a type and wraps it in a Promise, if it not already is one.
35 * This is to avoid `Promise<Promise<T>>`.
36 *
37 * This is the inverse of `Unpromisify<T>`.
38 */
39type Promisify<T> = T extends Promise<unknown> ? T : Promise<T>;
40/**
41 * Takes a type that may be Promise and unwraps the Promise type.
42 * If `P` is not a Promise, it returns `P`.
43 *
44 * This is the inverse of `Promisify<T>`.
45 */
46type Unpromisify<P> = P extends Promise<infer T> ? T : P;
47
48/**
49 * Takes the raw type of a remote property and returns the type that is visible to the local thread on the proxy.
50 *
51 * Note: This needs to be its own type alias, otherwise it will not distribute over unions.
52 * See https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
53 */
54type RemoteProperty<T> =
55 // If the value is a method, comlink will proxy it automatically.
56 // Objects are only proxied if they are marked to be proxied.
57 // Otherwise, the property is converted to a Promise that resolves the cloned value.
58 T extends Function | ProxyMarked ? Remote<T> : Promisify<T>;
59
60/**
61 * Takes the raw type of a property as a remote thread would see it through a proxy (e.g. when passed in as a function
62 * argument) and returns the type that the local thread has to supply.
63 *
64 * This is the inverse of `RemoteProperty<T>`.
65 *
66 * Note: This needs to be its own type alias, otherwise it will not distribute over unions. See
67 * https://www.typescriptlang.org/docs/handbook/advanced-types.html#distributive-conditional-types
68 */
69type LocalProperty<T> = T extends Function | ProxyMarked
70 ? Local<T>
71 : Unpromisify<T>;
72
73/**
74 * Proxies `T` if it is a `ProxyMarked`, clones it otherwise (as handled by structured cloning and transfer handlers).
75 */
76export type ProxyOrClone<T> = T extends ProxyMarked ? Remote<T> : T;
77/**
78 * Inverse of `ProxyOrClone<T>`.
79 */
80export type UnproxyOrClone<T> = T extends RemoteObject<ProxyMarked>
81 ? Local<T>
82 : T;
83
84/**
85 * Takes the raw type of a remote object in the other thread and returns the type as it is visible to the local thread
86 * when proxied with `Comlink.proxy()`.
87 *
88 * This does not handle call signatures, which is handled by the more general `Remote<T>` type.
89 *
90 * @template T The raw type of a remote object as seen in the other thread.
91 */
92export type RemoteObject<T> = { [P in keyof T]: RemoteProperty<T[P]> };
93/**
94 * Takes the type of an object as a remote thread would see it through a proxy (e.g. when passed in as a function
95 * argument) and returns the type that the local thread has to supply.
96 *
97 * This does not handle call signatures, which is handled by the more general `Local<T>` type.
98 *
99 * This is the inverse of `RemoteObject<T>`.
100 *
101 * @template T The type of a proxied object.
102 */
103export type LocalObject<T> = { [P in keyof T]: LocalProperty<T[P]> };
104
105/**
106 * Additional special comlink methods available on each proxy returned by `Comlink.wrap()`.
107 */
108export interface ProxyMethods {
109 [createEndpoint]: () => Promise<MessagePort>;
110 [releaseProxy]: () => void;
111}
112
113/**
114 * Takes the raw type of a remote object, function or class in the other thread and returns the type as it is visible to
115 * the local thread from the proxy return value of `Comlink.wrap()` or `Comlink.proxy()`.
116 */
117export type Remote<T> =
118 // Handle properties
119 RemoteObject<T> &
120 // Handle call signature (if present)
121 (T extends (...args: infer TArguments) => infer TReturn
122 ? (
123 ...args: { [I in keyof TArguments]: UnproxyOrClone<TArguments[I]> }
124 ) => Promisify<ProxyOrClone<Unpromisify<TReturn>>>
125 : unknown) &
126 // Handle construct signature (if present)
127 // The return of construct signatures is always proxied (whether marked or not)
128 (T extends { new (...args: infer TArguments): infer TInstance }
129 ? {
130 new (
131 ...args: {
132 [I in keyof TArguments]: UnproxyOrClone<TArguments[I]>;
133 }
134 ): Promisify<Remote<TInstance>>;
135 }
136 : unknown) &
137 // Include additional special comlink methods available on the proxy.
138 ProxyMethods;
139
140/**
141 * Expresses that a type can be either a sync or async.
142 */
143type MaybePromise<T> = Promise<T> | T;
144
145/**
146 * Takes the raw type of a remote object, function or class as a remote thread would see it through a proxy (e.g. when
147 * passed in as a function argument) and returns the type the local thread has to supply.
148 *
149 * This is the inverse of `Remote<T>`. It takes a `Remote<T>` and returns its original input `T`.
150 */
151export type Local<T> =
152 // Omit the special proxy methods (they don't need to be supplied, comlink adds them)
153 Omit<LocalObject<T>, keyof ProxyMethods> &
154 // Handle call signatures (if present)
155 (T extends (...args: infer TArguments) => infer TReturn
156 ? (
157 ...args: { [I in keyof TArguments]: ProxyOrClone<TArguments[I]> }
158 ) => // The raw function could either be sync or async, but is always proxied automatically
159 MaybePromise<UnproxyOrClone<Unpromisify<TReturn>>>
160 : unknown) &
161 // Handle construct signature (if present)
162 // The return of construct signatures is always proxied (whether marked or not)
163 (T extends { new (...args: infer TArguments): infer TInstance }
164 ? {
165 new (
166 ...args: {
167 [I in keyof TArguments]: ProxyOrClone<TArguments[I]>;
168 }
169 ): // The raw constructor could either be sync or async, but is always proxied automatically
170 MaybePromise<Local<Unpromisify<TInstance>>>;
171 }
172 : unknown);
173
174const isObject = (val: unknown): val is object =>
175 (typeof val === "object" && val !== null) || typeof val === "function";
176
177/**
178 * Customizes the serialization of certain values as determined by `canHandle()`.
179 *
180 * @template T The input type being handled by this transfer handler.
181 * @template S The serialized type sent over the wire.
182 */
183export interface TransferHandler<T, S> {
184 /**
185 * Gets called for every value to determine whether this transfer handler
186 * should serialize the value, which includes checking that it is of the right
187 * type (but can perform checks beyond that as well).
188 */
189 canHandle(value: unknown): value is T;
190
191 /**
192 * Gets called with the value if `canHandle()` returned `true` to produce a
193 * value that can be sent in a message, consisting of structured-cloneable
194 * values and/or transferrable objects.
195 */
196 serialize(value: T): [S, Transferable[]];
197
198 /**
199 * Gets called to deserialize an incoming value that was serialized in the
200 * other thread with this transfer handler (known through the name it was
201 * registered under).
202 */
203 deserialize(value: S): T;
204}
205
206/**
207 * Internal transfer handle to handle objects marked to proxy.
208 */
209const proxyTransferHandler: TransferHandler<object, MessagePort> = {
210 canHandle: (val): val is ProxyMarked =>
211 isObject(val) && (val as ProxyMarked)[proxyMarker],
212 serialize(obj) {
213 const { port1, port2 } = new MessageChannel();
214 expose(obj, port1);
215 return [port2, [port2]];
216 },
217 deserialize(port) {
218 port.start();
219 return wrap(port);
220 },
221};
222
223interface ThrownValue {
224 [throwMarker]: unknown; // just needs to be present
225 value: unknown;
226}
227type SerializedThrownValue =
228 | { isError: true; value: Error }
229 | { isError: false; value: unknown };
230
231/**
232 * Internal transfer handler to handle thrown exceptions.
233 */
234const throwTransferHandler: TransferHandler<
235 ThrownValue,
236 SerializedThrownValue
237> = {
238 canHandle: (value): value is ThrownValue =>
239 isObject(value) && throwMarker in value,
240 serialize({ value }) {
241 let serialized: SerializedThrownValue;
242 if (value instanceof Error) {
243 serialized = {
244 isError: true,
245 value: {
246 message: value.message,
247 name: value.name,
248 stack: value.stack,
249 },
250 };
251 } else {
252 serialized = { isError: false, value };
253 }
254 return [serialized, []];
255 },
256 deserialize(serialized) {
257 if (serialized.isError) {
258 throw Object.assign(
259 new Error(serialized.value.message),
260 serialized.value
261 );
262 }
263 throw serialized.value;
264 },
265};
266
267/**
268 * Allows customizing the serialization of certain values.
269 */
270export const transferHandlers = new Map<
271 string,
272 TransferHandler<unknown, unknown>
273>([
274 ["proxy", proxyTransferHandler],
275 ["throw", throwTransferHandler],
276]);
277
278function isAllowedOrigin(
279 allowedOrigins: (string | RegExp)[],
280 origin: string
281): boolean {
282 for (const allowedOrigin of allowedOrigins) {
283 if (origin === allowedOrigin || allowedOrigin === "*") {
284 return true;
285 }
286 if (allowedOrigin instanceof RegExp && allowedOrigin.test(origin)) {
287 return true;
288 }
289 }
290 return false;
291}
292
293export function expose(
294 obj: any,
295 ep: Endpoint = globalThis as any,
296 allowedOrigins: (string | RegExp)[] = ["*"]
297) {
298 ep.addEventListener("message", function callback(ev: MessageEvent) {
299 if (!ev || !ev.data) {
300 return;
301 }
302 if (!isAllowedOrigin(allowedOrigins, ev.origin)) {
303 console.warn(`Invalid origin '${ev.origin}' for comlink proxy`);
304 return;
305 }
306 const { id, type, path } = {
307 path: [] as string[],
308 ...(ev.data as Message),
309 };
310 const argumentList = (ev.data.argumentList || []).map(fromWireValue);
311 let returnValue;
312 try {
313 const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj);
314 const rawValue = path.reduce((obj, prop) => obj[prop], obj);
315 switch (type) {
316 case MessageType.GET:
317 {
318 returnValue = rawValue;
319 }
320 break;
321 case MessageType.SET:
322 {
323 parent[path.slice(-1)[0]] = fromWireValue(ev.data.value);
324 returnValue = true;
325 }
326 break;
327 case MessageType.APPLY:
328 {
329 returnValue = rawValue.apply(parent, argumentList);
330 }
331 break;
332 case MessageType.CONSTRUCT:
333 {
334 const value = new rawValue(...argumentList);
335 returnValue = proxy(value);
336 }
337 break;
338 case MessageType.ENDPOINT:
339 {
340 const { port1, port2 } = new MessageChannel();
341 expose(obj, port2);
342 returnValue = transfer(port1, [port1]);
343 }
344 break;
345 case MessageType.RELEASE:
346 {
347 returnValue = undefined;
348 }
349 break;
350 default:
351 return;
352 }
353 } catch (value) {
354 returnValue = { value, [throwMarker]: 0 };
355 }
356 Promise.resolve(returnValue)
357 .catch((value) => {
358 return { value, [throwMarker]: 0 };
359 })
360 .then((returnValue) => {
361 const [wireValue, transferables] = toWireValue(returnValue);
362 ep.postMessage({ ...wireValue, id }, transferables);
363 if (type === MessageType.RELEASE) {
364 // detach and deactive after sending release response above.
365 ep.removeEventListener("message", callback as any);
366 closeEndPoint(ep);
367 if (finalizer in obj && typeof obj[finalizer] === "function") {
368 obj[finalizer]();
369 }
370 }
371 })
372 .catch((error) => {
373 // Send Serialization Error To Caller
374 const [wireValue, transferables] = toWireValue({
375 value: new TypeError("Unserializable return value"),
376 [throwMarker]: 0,
377 });
378 ep.postMessage({ ...wireValue, id }, transferables);
379 });
380 } as any);
381 if (ep.start) {
382 ep.start();
383 }
384}
385
386function isMessagePort(endpoint: Endpoint): endpoint is MessagePort {
387 return endpoint.constructor.name === "MessagePort";
388}
389
390function closeEndPoint(endpoint: Endpoint) {
391 if (isMessagePort(endpoint)) endpoint.close();
392}
393
394export function wrap<T>(ep: Endpoint, target?: any): Remote<T> {
395 return createProxy<T>(ep, [], target) as any;
396}
397
398function throwIfProxyReleased(isReleased: boolean) {
399 if (isReleased) {
400 throw new Error("Proxy has been released and is not useable");
401 }
402}
403
404function releaseEndpoint(ep: Endpoint) {
405 return requestResponseMessage(ep, {
406 type: MessageType.RELEASE,
407 }).then(() => {
408 closeEndPoint(ep);
409 });
410}
411
412interface FinalizationRegistry<T> {
413 new (cb: (heldValue: T) => void): FinalizationRegistry<T>;
414 register(
415 weakItem: object,
416 heldValue: T,
417 unregisterToken?: object | undefined
418 ): void;
419 unregister(unregisterToken: object): void;
420}
421declare var FinalizationRegistry: FinalizationRegistry<Endpoint>;
422
423const proxyCounter = new WeakMap<Endpoint, number>();
424const proxyFinalizers =
425 "FinalizationRegistry" in globalThis &&
426 new FinalizationRegistry((ep: Endpoint) => {
427 const newCount = (proxyCounter.get(ep) || 0) - 1;
428 proxyCounter.set(ep, newCount);
429 if (newCount === 0) {
430 releaseEndpoint(ep);
431 }
432 });
433
434function registerProxy(proxy: object, ep: Endpoint) {
435 const newCount = (proxyCounter.get(ep) || 0) + 1;
436 proxyCounter.set(ep, newCount);
437 if (proxyFinalizers) {
438 proxyFinalizers.register(proxy, ep, proxy);
439 }
440}
441
442function unregisterProxy(proxy: object) {
443 if (proxyFinalizers) {
444 proxyFinalizers.unregister(proxy);
445 }
446}
447
448function createProxy<T>(
449 ep: Endpoint,
450 path: (string | number | symbol)[] = [],
451 target: object = function () {}
452): Remote<T> {
453 let isProxyReleased = false;
454 const proxy = new Proxy(target, {
455 get(_target, prop) {
456 throwIfProxyReleased(isProxyReleased);
457 if (prop === releaseProxy) {
458 return () => {
459 unregisterProxy(proxy);
460 releaseEndpoint(ep);
461 isProxyReleased = true;
462 };
463 }
464 if (prop === "then") {
465 if (path.length === 0) {
466 return { then: () => proxy };
467 }
468 const r = requestResponseMessage(ep, {
469 type: MessageType.GET,
470 path: path.map((p) => p.toString()),
471 }).then(fromWireValue);
472 return r.then.bind(r);
473 }
474 return createProxy(ep, [...path, prop]);
475 },
476 set(_target, prop, rawValue) {
477 throwIfProxyReleased(isProxyReleased);
478 // FIXME: ES6 Proxy Handler `set` methods are supposed to return a
479 // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯
480 const [value, transferables] = toWireValue(rawValue);
481 return requestResponseMessage(
482 ep,
483 {
484 type: MessageType.SET,
485 path: [...path, prop].map((p) => p.toString()),
486 value,
487 },
488 transferables
489 ).then(fromWireValue) as any;
490 },
491 apply(_target, _thisArg, rawArgumentList) {
492 throwIfProxyReleased(isProxyReleased);
493 const last = path[path.length - 1];
494 if ((last as any) === createEndpoint) {
495 return requestResponseMessage(ep, {
496 type: MessageType.ENDPOINT,
497 }).then(fromWireValue);
498 }
499 // We just pretend that `bind()` didn’t happen.
500 if (last === "bind") {
501 return createProxy(ep, path.slice(0, -1));
502 }
503 const [argumentList, transferables] = processArguments(rawArgumentList);
504 return requestResponseMessage(
505 ep,
506 {
507 type: MessageType.APPLY,
508 path: path.map((p) => p.toString()),
509 argumentList,
510 },
511 transferables
512 ).then(fromWireValue);
513 },
514 construct(_target, rawArgumentList) {
515 throwIfProxyReleased(isProxyReleased);
516 const [argumentList, transferables] = processArguments(rawArgumentList);
517 return requestResponseMessage(
518 ep,
519 {
520 type: MessageType.CONSTRUCT,
521 path: path.map((p) => p.toString()),
522 argumentList,
523 },
524 transferables
525 ).then(fromWireValue);
526 },
527 });
528 registerProxy(proxy, ep);
529 return proxy as any;
530}
531
532function myFlat<T>(arr: (T | T[])[]): T[] {
533 return Array.prototype.concat.apply([], arr);
534}
535
536function processArguments(argumentList: any[]): [WireValue[], Transferable[]] {
537 const processed = argumentList.map(toWireValue);
538 return [processed.map((v) => v[0]), myFlat(processed.map((v) => v[1]))];
539}
540
541const transferCache = new WeakMap<any, Transferable[]>();
542export function transfer<T>(obj: T, transfers: Transferable[]): T {
543 transferCache.set(obj, transfers);
544 return obj;
545}
546
547export function proxy<T extends {}>(obj: T): T & ProxyMarked {
548 return Object.assign(obj, { [proxyMarker]: true }) as any;
549}
550
551export function windowEndpoint(
552 w: PostMessageWithOrigin,
553 context: EventSource = globalThis,
554 targetOrigin = "*"
555): Endpoint {
556 return {
557 postMessage: (msg: any, transferables: Transferable[]) =>
558 w.postMessage(msg, targetOrigin, transferables),
559 addEventListener: context.addEventListener.bind(context),
560 removeEventListener: context.removeEventListener.bind(context),
561 };
562}
563
564function toWireValue(value: any): [WireValue, Transferable[]] {
565 for (const [name, handler] of transferHandlers) {
566 if (handler.canHandle(value)) {
567 const [serializedValue, transferables] = handler.serialize(value);
568 return [
569 {
570 type: WireValueType.HANDLER,
571 name,
572 value: serializedValue,
573 },
574 transferables,
575 ];
576 }
577 }
578 return [
579 {
580 type: WireValueType.RAW,
581 value,
582 },
583 transferCache.get(value) || [],
584 ];
585}
586
587function fromWireValue(value: WireValue): any {
588 switch (value.type) {
589 case WireValueType.HANDLER:
590 return transferHandlers.get(value.name)!.deserialize(value.value);
591 case WireValueType.RAW:
592 return value.value;
593 }
594}
595
596function requestResponseMessage(
597 ep: Endpoint,
598 msg: Message,
599 transfers?: Transferable[]
600): Promise<WireValue> {
601 return new Promise((resolve) => {
602 const id = generateUUID();
603 ep.addEventListener("message", function l(ev: MessageEvent) {
604 if (!ev.data || !ev.data.id || ev.data.id !== id) {
605 return;
606 }
607 ep.removeEventListener("message", l as any);
608 resolve(ev.data);
609 } as any);
610 if (ep.start) {
611 ep.start();
612 }
613 ep.postMessage({ id, ...msg }, transfers);
614 });
615}
616
617function generateUUID(): string {
618 return new Array(4)
619 .fill(0)
620 .map(() => Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(16))
621 .join("-");
622}