1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | import {
|
15 | Endpoint,
|
16 | EventSource,
|
17 | Message,
|
18 | MessageType,
|
19 | PostMessageWithOrigin,
|
20 | WireValue,
|
21 | WireValueType
|
22 | } from "./protocol.js";
|
23 | export { Endpoint };
|
24 |
|
25 | export const proxyMarker = Symbol("Comlink.proxy");
|
26 | export const createEndpoint = Symbol("Comlink.endpoint");
|
27 | export const releaseProxy = Symbol("Comlink.releaseProxy");
|
28 | const throwSet = new WeakSet();
|
29 |
|
30 |
|
31 | type 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 |
|
40 | export 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 |
|
51 | export interface TransferHandler {
|
52 | canHandle(obj: any): boolean;
|
53 | serialize(obj: any): [any, Transferable[]];
|
54 | deserialize(obj: any): any;
|
55 | }
|
56 |
|
57 | export 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 |
|
99 | export 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 |
|
157 | ep.removeEventListener("message", callback as any);
|
158 | closeEndPoint(ep);
|
159 | }
|
160 | } as any);
|
161 | if (ep.start) {
|
162 | ep.start();
|
163 | }
|
164 | }
|
165 |
|
166 | function isMessagePort(endpoint: Endpoint): endpoint is MessagePort {
|
167 | return endpoint.constructor.name === "MessagePort";
|
168 | }
|
169 |
|
170 | function closeEndPoint(endpoint: Endpoint) {
|
171 | if (isMessagePort(endpoint)) endpoint.close();
|
172 | }
|
173 |
|
174 | export function wrap<T>(ep: Endpoint): Remote<T> {
|
175 | return createProxy<T>(ep) as any;
|
176 | }
|
177 |
|
178 | function throwIfProxyReleased(isReleased: boolean) {
|
179 | if (isReleased) {
|
180 | throw new Error("Proxy has been released and is not useable");
|
181 | }
|
182 | }
|
183 |
|
184 | function 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 |
|
218 |
|
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 |
|
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 |
|
270 | function myFlat<T>(arr: (T | T[])[]): T[] {
|
271 | return Array.prototype.concat.apply([], arr);
|
272 | }
|
273 |
|
274 | function 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 |
|
279 | const transferCache = new WeakMap<any, Transferable[]>();
|
280 | export function transfer(obj: any, transfers: Transferable[]) {
|
281 | transferCache.set(obj, transfers);
|
282 | return obj;
|
283 | }
|
284 |
|
285 | export function proxy<T>(obj: T): T & { [proxyMarker]: true } {
|
286 | return Object.assign(obj, { [proxyMarker]: true }) as any;
|
287 | }
|
288 |
|
289 | export 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 |
|
301 | function 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 |
|
324 | function 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 |
|
333 | function 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 |
|
354 | function 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 | }
|