1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | import {
|
8 | Endpoint,
|
9 | EventSource,
|
10 | Message,
|
11 | MessageType,
|
12 | PostMessageWithOrigin,
|
13 | WireValue,
|
14 | WireValueType,
|
15 | } from "./protocol";
|
16 | export type { Endpoint };
|
17 |
|
18 | export const proxyMarker = Symbol("Comlink.proxy");
|
19 | export const createEndpoint = Symbol("Comlink.endpoint");
|
20 | export const releaseProxy = Symbol("Comlink.releaseProxy");
|
21 | export const finalizer = Symbol("Comlink.finalizer");
|
22 |
|
23 | const throwMarker = Symbol("Comlink.thrown");
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 | export interface ProxyMarked {
|
30 | [proxyMarker]: true;
|
31 | }
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 | type Promisify<T> = T extends Promise<unknown> ? T : Promise<T>;
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 | type Unpromisify<P> = P extends Promise<infer T> ? T : P;
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 |
|
54 | type RemoteProperty<T> =
|
55 |
|
56 |
|
57 |
|
58 | T extends Function | ProxyMarked ? Remote<T> : Promisify<T>;
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 | type LocalProperty<T> = T extends Function | ProxyMarked
|
70 | ? Local<T>
|
71 | : Unpromisify<T>;
|
72 |
|
73 |
|
74 |
|
75 |
|
76 | export type ProxyOrClone<T> = T extends ProxyMarked ? Remote<T> : T;
|
77 |
|
78 |
|
79 |
|
80 | export type UnproxyOrClone<T> = T extends RemoteObject<ProxyMarked>
|
81 | ? Local<T>
|
82 | : T;
|
83 |
|
84 |
|
85 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | export type RemoteObject<T> = { [P in keyof T]: RemoteProperty<T[P]> };
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 | export type LocalObject<T> = { [P in keyof T]: LocalProperty<T[P]> };
|
104 |
|
105 |
|
106 |
|
107 |
|
108 | export interface ProxyMethods {
|
109 | [createEndpoint]: () => Promise<MessagePort>;
|
110 | [releaseProxy]: () => void;
|
111 | }
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 | export type Remote<T> =
|
118 |
|
119 | RemoteObject<T> &
|
120 |
|
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 |
|
127 |
|
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 |
|
138 | ProxyMethods;
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | type MaybePromise<T> = Promise<T> | T;
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 |
|
151 | export type Local<T> =
|
152 |
|
153 | Omit<LocalObject<T>, keyof ProxyMethods> &
|
154 |
|
155 | (T extends (...args: infer TArguments) => infer TReturn
|
156 | ? (
|
157 | ...args: { [I in keyof TArguments]: ProxyOrClone<TArguments[I]> }
|
158 | ) =>
|
159 | MaybePromise<UnproxyOrClone<Unpromisify<TReturn>>>
|
160 | : unknown) &
|
161 |
|
162 |
|
163 | (T extends { new (...args: infer TArguments): infer TInstance }
|
164 | ? {
|
165 | new (
|
166 | ...args: {
|
167 | [I in keyof TArguments]: ProxyOrClone<TArguments[I]>;
|
168 | }
|
169 | ):
|
170 | MaybePromise<Local<Unpromisify<TInstance>>>;
|
171 | }
|
172 | : unknown);
|
173 |
|
174 | const isObject = (val: unknown): val is object =>
|
175 | (typeof val === "object" && val !== null) || typeof val === "function";
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | export interface TransferHandler<T, S> {
|
184 | |
185 |
|
186 |
|
187 |
|
188 |
|
189 | canHandle(value: unknown): value is T;
|
190 |
|
191 | |
192 |
|
193 |
|
194 |
|
195 |
|
196 | serialize(value: T): [S, Transferable[]];
|
197 |
|
198 | |
199 |
|
200 |
|
201 |
|
202 |
|
203 | deserialize(value: S): T;
|
204 | }
|
205 |
|
206 |
|
207 |
|
208 |
|
209 | const 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 |
|
223 | interface ThrownValue {
|
224 | [throwMarker]: unknown;
|
225 | value: unknown;
|
226 | }
|
227 | type SerializedThrownValue =
|
228 | | { isError: true; value: Error }
|
229 | | { isError: false; value: unknown };
|
230 |
|
231 |
|
232 |
|
233 |
|
234 | const 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 |
|
269 |
|
270 | export const transferHandlers = new Map<
|
271 | string,
|
272 | TransferHandler<unknown, unknown>
|
273 | >([
|
274 | ["proxy", proxyTransferHandler],
|
275 | ["throw", throwTransferHandler],
|
276 | ]);
|
277 |
|
278 | function 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 |
|
293 | export 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 |
|
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 |
|
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 |
|
386 | function isMessagePort(endpoint: Endpoint): endpoint is MessagePort {
|
387 | return endpoint.constructor.name === "MessagePort";
|
388 | }
|
389 |
|
390 | function closeEndPoint(endpoint: Endpoint) {
|
391 | if (isMessagePort(endpoint)) endpoint.close();
|
392 | }
|
393 |
|
394 | export function wrap<T>(ep: Endpoint, target?: any): Remote<T> {
|
395 | return createProxy<T>(ep, [], target) as any;
|
396 | }
|
397 |
|
398 | function throwIfProxyReleased(isReleased: boolean) {
|
399 | if (isReleased) {
|
400 | throw new Error("Proxy has been released and is not useable");
|
401 | }
|
402 | }
|
403 |
|
404 | function releaseEndpoint(ep: Endpoint) {
|
405 | return requestResponseMessage(ep, {
|
406 | type: MessageType.RELEASE,
|
407 | }).then(() => {
|
408 | closeEndPoint(ep);
|
409 | });
|
410 | }
|
411 |
|
412 | interface 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 | }
|
421 | declare var FinalizationRegistry: FinalizationRegistry<Endpoint>;
|
422 |
|
423 | const proxyCounter = new WeakMap<Endpoint, number>();
|
424 | const 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 |
|
434 | function 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 |
|
442 | function unregisterProxy(proxy: object) {
|
443 | if (proxyFinalizers) {
|
444 | proxyFinalizers.unregister(proxy);
|
445 | }
|
446 | }
|
447 |
|
448 | function 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 |
|
479 |
|
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 |
|
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 |
|
532 | function myFlat<T>(arr: (T | T[])[]): T[] {
|
533 | return Array.prototype.concat.apply([], arr);
|
534 | }
|
535 |
|
536 | function 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 |
|
541 | const transferCache = new WeakMap<any, Transferable[]>();
|
542 | export function transfer<T>(obj: T, transfers: Transferable[]): T {
|
543 | transferCache.set(obj, transfers);
|
544 | return obj;
|
545 | }
|
546 |
|
547 | export function proxy<T extends {}>(obj: T): T & ProxyMarked {
|
548 | return Object.assign(obj, { [proxyMarker]: true }) as any;
|
549 | }
|
550 |
|
551 | export 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 |
|
564 | function 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 |
|
587 | function 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 |
|
596 | function 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 |
|
617 | function 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 | }
|