From f7913e3a02905389eeee4fda441356f3dbb19688 Mon Sep 17 00:00:00 2001 From: Jarek Radosz Date: Wed, 19 Jun 2019 02:07:26 +0200 Subject: [PATCH] Add wrapChain function --- src/comlink.ts | 350 ++++++++++++++---- src/protocol.ts | 18 +- tests/same_window.comlink.test.js | 10 +- tests/worker-chain.comlink.test.js | 564 +++++++++++++++++++++++++++++ 4 files changed, 855 insertions(+), 87 deletions(-) create mode 100644 tests/worker-chain.comlink.test.js diff --git a/src/comlink.ts b/src/comlink.ts index a623b1c5..6cdfde5b 100644 --- a/src/comlink.ts +++ b/src/comlink.ts @@ -12,11 +12,18 @@ */ import { + ApplyMessage, + Command, + ConstructMessage, Endpoint, + EndpointMessage, EventSource, + GetMessage, Message, MessageType, + Prop, PostMessageWithOrigin, + SetMessage, WireValue, WireValueType } from "./protocol.js"; @@ -95,60 +102,77 @@ export const transferHandlers = new Map([ ] ]); -export function expose(obj: any, ep: Endpoint = self as any) { +export function expose(object: any, ep: Endpoint = self as any) { ep.addEventListener("message", (async (ev: MessageEvent) => { if (!ev || !ev.data) { return; } - const { id, type, path } = { - path: [] as string[], - ...(ev.data as Message) - }; - const argumentList = (ev.data.argumentList || []).map(fromWireValue); - let returnValue; + + const { id, messages } = ev.data as Command; + let currentObject = object; + let previousObject = object; + let returnValue = object; + try { - const parent = path.slice(0, -1).reduce((obj, prop) => obj[prop], obj); - const rawValue = path.reduce((obj, prop) => obj[prop], obj); - switch (type) { - case MessageType.GET: - { - returnValue = await rawValue; - } - break; - case MessageType.SET: - { - parent[path.slice(-1)[0]] = fromWireValue(ev.data.value); - returnValue = true; - } - break; - case MessageType.APPLY: - { - returnValue = await rawValue.apply(parent, argumentList); - } - break; - case MessageType.CONSTRUCT: - { - const value = await new rawValue(...argumentList); - returnValue = proxy(value); - } - break; - case MessageType.ENDPOINT: - { - const { port1, port2 } = new MessageChannel(); - expose(obj, port2); - returnValue = transfer(port1, [port1]); - } - break; - default: - console.warn("Unrecognized message", ev.data); + for (const message of messages) { + const argumentList = ((message as any).argumentList || []).map( + fromWireValue + ); + const { type } = message; + + switch (type) { + case MessageType.GET: + { + previousObject = currentObject; + returnValue = currentObject = await currentObject[ + (message as GetMessage).prop + ]; + } + break; + case MessageType.SET: + { + currentObject[(message as SetMessage).prop] = fromWireValue( + (message as SetMessage).value + ); + returnValue = true; + } + break; + case MessageType.APPLY: + { + returnValue = currentObject = await currentObject.apply( + previousObject, + argumentList + ); + previousObject = currentObject; + } + break; + case MessageType.CONSTRUCT: + { + currentObject = await new currentObject(...argumentList); + previousObject = currentObject; + returnValue = proxy(currentObject); + } + break; + case MessageType.ENDPOINT: + { + const { port1, port2 } = new MessageChannel(); + expose(currentObject, port2); + returnValue = transfer(port1, [port1]); + } + break; + default: + console.warn("Unrecognized message", ev.data); + } } } catch (e) { returnValue = e; throwSet.add(e); } + const [wireValue, transferables] = toWireValue(returnValue); ep.postMessage({ ...wireValue, id }, transferables); }) as any); + if (ep.start) { ep.start(); } @@ -158,20 +182,20 @@ export function wrap(ep: Endpoint): Remote { return createProxy(ep) as any; } -function createProxy( - ep: Endpoint, - path: (string | number | symbol)[] = [] -): Remote { +function createProxy(ep: Endpoint, path: Prop[] = []): Remote { const proxy: Function = new Proxy(function() {}, { get(_target, prop) { if (prop === "then") { if (path.length === 0) { return { then: () => proxy }; } - const r = requestResponseMessage(ep, { - type: MessageType.GET, - path: path.map(p => p.toString()) - }).then(fromWireValue); + const r = requestResponseMessage( + ep, + path.map(prop => ({ + type: MessageType.GET, + prop + })) + ).then(fromWireValue); return r.then.bind(r); } return createProxy(ep, [...path, prop]); @@ -180,49 +204,215 @@ function createProxy( // FIXME: ES6 Proxy Handler `set` methods are supposed to return a // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ const [value, transferables] = toWireValue(rawValue); - return requestResponseMessage( - ep, + const messages = [ + ...path.map( + prop => + ({ + type: MessageType.GET, + prop + } as Message) + ), { type: MessageType.SET, - path: [...path, prop].map(p => p.toString()), + prop, value - }, - transferables - ).then(fromWireValue) as any; + } as SetMessage + ]; + return requestResponseMessage(ep, messages, transferables).then( + fromWireValue + ) as any; }, apply(_target, _thisArg, rawArgumentList) { const last = path[path.length - 1]; if ((last as any) === createEndpoint) { - return requestResponseMessage(ep, { - type: MessageType.ENDPOINT - }).then(fromWireValue); + const messages = [ + { + type: MessageType.ENDPOINT + } as EndpointMessage + ]; + return requestResponseMessage(ep, messages).then(fromWireValue); } // We just pretend that `bind()` didn’t happen. if (last === "bind") { return createProxy(ep, path.slice(0, -1)); } const [argumentList, transferables] = processArguments(rawArgumentList); - return requestResponseMessage( - ep, + const messages = [ + ...path.map( + prop => + ({ + type: MessageType.GET, + prop + } as Message) + ), { type: MessageType.APPLY, - path: path.map(p => p.toString()), argumentList - }, - transferables - ).then(fromWireValue); + } as ApplyMessage + ]; + return requestResponseMessage(ep, messages, transferables).then( + fromWireValue + ); }, construct(_target, rawArgumentList) { const [argumentList, transferables] = processArguments(rawArgumentList); - return requestResponseMessage( - ep, + const messages = [ + ...path.map( + prop => + ({ + type: MessageType.GET, + prop + } as Message) + ), { type: MessageType.CONSTRUCT, - path: path.map(p => p.toString()), argumentList - }, + } as ConstructMessage + ]; + return requestResponseMessage(ep, messages, transferables).then( + fromWireValue + ); + } + }); + return proxy as any; +} + +export function wrapChain(ep: Endpoint): Remote { + return createChainProxy(ep) as any; +} + +function createChainProxy( + ep: Endpoint, + messages: (Message)[] = [], + previousTransferables: Transferable[] = [] +): Remote { + const proxy: Function = new Proxy(function() {}, { + get(_target, prop) { + if (prop === "then") { + if (messages.length === 0) { + return { then: () => proxy }; + } + + const r = requestResponseMessage( + ep, + messages, + previousTransferables + ).then(fromWireValue); + return r.then.bind(r); + } + + return createChainProxy( + ep, + [ + ...messages, + { + type: MessageType.GET, + prop + } + ], + previousTransferables + ); + }, + set(_target, prop, rawValue) { + // FIXME: ES6 Proxy Handler `set` methods are supposed to return a + // boolean. To show good will, we return true asynchronously ¯\_(ツ)_/¯ + const [value, newTransferables] = toWireValue(rawValue); + const transferables = [...previousTransferables, ...newTransferables]; + + return requestResponseMessage( + ep, + [ + ...messages, + { + type: MessageType.SET, + prop, + value + } + ], transferables - ).then(fromWireValue); + ).then(fromWireValue) as any; + }, + apply(_target, _thisArg, rawArgumentList) { + const last: Message | undefined = messages[messages.length - 1]; + + if ( + last && + last.type === MessageType.GET && + last.prop === createEndpoint + ) { + return requestResponseMessage( + ep, + [ + ...messages.slice(0, -1), + { + type: MessageType.ENDPOINT + } + ], + previousTransferables + ).then(fromWireValue); + } + + // We just pretend that `bind()` didn’t happen. + if (last && last.type === MessageType.GET && last.prop === "bind") { + return createChainProxy( + ep, + messages.slice(0, -1), + previousTransferables + ); + } + + if (last && last.type === MessageType.GET && last.prop === "then") { + const [argumentList, newTransferables] = processArguments( + rawArgumentList + ); + const transferables = [...previousTransferables, ...newTransferables]; + return requestResponseMessage( + ep, + [ + ...messages, + { + type: MessageType.APPLY, + argumentList + } + ], + transferables + ).then(fromWireValue); + } + + const [argumentList, newTransferables] = processArguments( + rawArgumentList + ); + const transferables = [...previousTransferables, ...newTransferables]; + + return createChainProxy( + ep, + [ + ...messages, + { + type: MessageType.APPLY, + argumentList + } + ], + transferables + ); + }, + construct(_target, rawArgumentList) { + const [argumentList, newTransferables] = processArguments( + rawArgumentList + ); + const transferables = [...previousTransferables, ...newTransferables]; + + return createChainProxy( + ep, + [ + ...messages, + { + type: MessageType.CONSTRUCT, + argumentList + } + ], + transferables + ); } }); return proxy as any; @@ -293,22 +483,40 @@ function fromWireValue(value: WireValue): any { function requestResponseMessage( ep: Endpoint, - msg: Message, + messages: Message[], transfers?: Transferable[] ): Promise { return new Promise(resolve => { const id = generateUUID(); + ep.addEventListener("message", function l(ev: MessageEvent) { if (!ev.data || !ev.data.id || ev.data.id !== id) { return; } + ep.removeEventListener("message", l as any); resolve(ev.data); } as any); + if (ep.start) { ep.start(); } - ep.postMessage({ id, ...msg }, transfers); + + ep.postMessage( + { + id, + messages: messages.map(message => { + const m = { ...message }; + + if (m.type === MessageType.GET || m.type === MessageType.SET) { + m.prop = m.prop.toString(); + } + + return m; + }) + } as Command, + transfers + ); }); } diff --git a/src/protocol.ts b/src/protocol.ts index 8df8d29d..5f159870 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -59,6 +59,8 @@ export interface HandlerWireValue { export type WireValue = RawWireValue | HandlerWireValue; +export type Prop = string | number | symbol; + export type MessageID = string; export const enum MessageType { @@ -70,34 +72,27 @@ export const enum MessageType { } export interface GetMessage { - id?: MessageID; type: MessageType.GET; - path: string[]; + prop: Prop; } export interface SetMessage { - id?: MessageID; type: MessageType.SET; - path: string[]; + prop: Prop; value: WireValue; } export interface ApplyMessage { - id?: MessageID; type: MessageType.APPLY; - path: string[]; argumentList: WireValue[]; } export interface ConstructMessage { - id?: MessageID; type: MessageType.CONSTRUCT; - path: string[]; argumentList: WireValue[]; } export interface EndpointMessage { - id?: MessageID; type: MessageType.ENDPOINT; } @@ -107,3 +102,8 @@ export type Message = | ApplyMessage | ConstructMessage | EndpointMessage; + +export interface Command { + id: MessageID; + messages: Message[]; +} diff --git a/tests/same_window.comlink.test.js b/tests/same_window.comlink.test.js index b529242a..c1a9f134 100644 --- a/tests/same_window.comlink.test.js +++ b/tests/same_window.comlink.test.js @@ -382,12 +382,9 @@ describe("Comlink in the same realm", function() { it("will wrap marked parameter values, simple function", async function() { const thing = Comlink.wrap(this.port1); Comlink.expose(async function(f) { - await f(); + return await f(); }, this.port2); - // Weird code because Mocha - await new Promise(async resolve => { - thing(Comlink.proxy(_ => resolve())); - }); + expect(await thing(Comlink.proxy(_ => 1))).to.equal(1); }); it("will wrap multiple marked parameter values, simple function", async function() { @@ -395,7 +392,6 @@ describe("Comlink in the same realm", function() { Comlink.expose(async function(f1, f2, f3) { return (await f1()) + (await f2()) + (await f3()); }, this.port2); - // Weird code because Mocha expect( await thing( Comlink.proxy(_ => 1), @@ -478,7 +474,7 @@ describe("Comlink in the same realm", function() { port2.postMessage({ a: 1 }); }); - it("can tunnels a new endpoint with createEndpoint", async function() { + it("can tunnel a new endpoint with createEndpoint", async function() { Comlink.expose( { a: 4, diff --git a/tests/worker-chain.comlink.test.js b/tests/worker-chain.comlink.test.js new file mode 100644 index 00000000..588f3dad --- /dev/null +++ b/tests/worker-chain.comlink.test.js @@ -0,0 +1,564 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as Comlink from "/base/dist/esm/comlink.mjs"; + +function chainSample() { + return { + get prop1() { + return this; + }, + + get prop2() { + return this; + }, + + method1() { + return this; + }, + + method2() { + return this; + }, + + method3() { + return "done"; + } + }; +} + +class SampleClass { + constructor(counterInit = 1) { + this._counter = counterInit; + this._promise = Promise.resolve(4); + } + + static get SOME_NUMBER() { + return 4; + } + + static ADD(a, b) { + return a + b; + } + + get counter() { + return this._counter; + } + + set counter(value) { + this._counter = value; + } + + get promise() { + return this._promise; + } + + method() { + return 4; + } + + increaseCounter(delta = 1) { + this._counter += delta; + } + + promiseFunc() { + return new Promise(resolve => setTimeout(_ => resolve(4), 100)); + } + + proxyFunc() { + return Comlink.proxy({ + counter: 0, + inc() { + this.counter++; + } + }); + } + + throwsAnError() { + throw Error("OMG"); + } +} + +describe("Comlink.wrapChain()", function() { + beforeEach(function() { + const { port1, port2 } = new MessageChannel(); + port1.start(); + port2.start(); + this.port1 = port1; + this.port2 = port2; + }); + + describe("Chain-specific", function() { + it("supports both getters and method calls", async function() { + const chainThing = Comlink.wrapChain(this.port1); + Comlink.expose(chainSample, this.port2); + const result = await chainThing() + .method1() + .prop1 + .prop2 + .method2() + .method3(); + + expect(result).to.equal('done'); + }); + + it.only("supports constructors and setters", async function() { + let counter = 0; + class ChainClass { + constructor() { + counter++; + } + + set setter(value) { + counter += value; + } + } + + ChainClass.sum = () => counter; + + const ChainThing = Comlink.wrapChain(this.port1); + Comlink.expose(ChainClass, this.port2); + await (new ChainThing().setter = 2); + + expect(await ChainThing.sum()).to.equal(3); + }); + }); + + describe("basic wrap() functionality", function() { + it("can work with objects", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose({ value: 4 }, this.port2); + expect(await thing.value).to.equal(4); + }); + + it("can work functions on an object", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose({ f: _ => 4 }, this.port2); + expect(await thing.f()).to.equal(4); + }); + + it("can work with functions", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(_ => 4, this.port2); + expect(await thing()).to.equal(4); + }); + + it("can work with objects that have undefined properties", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose({ x: undefined }, this.port2); + expect(await thing.x).to.be.undefined; + }); + + it("can keep the stack and message of thrown errors", async function() { + let stack; + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(_ => { + const error = Error("OMG"); + stack = error.stack; + throw error; + }, this.port2); + try { + await thing(); + throw "Should have thrown"; + } catch (err) { + expect(err).to.not.eq("Should have thrown"); + expect(err.message).to.equal("OMG"); + expect(err.stack).to.equal(stack); + } + }); + + it("can rethrow non-error objects", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(_ => { + throw { test: true }; + }, this.port2); + try { + await thing(); + throw "Should have thrown"; + } catch (err) { + expect(err).to.not.eq("Should have thrown"); + expect(err.test).to.equal(true); + } + }); + + it("can work with parameterized functions", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose((a, b) => a + b, this.port2); + expect(await thing(1, 3)).to.equal(4); + }); + + it("can work with functions that return promises", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose( + _ => new Promise(resolve => setTimeout(_ => resolve(4), 100)), + this.port2 + ); + expect(await thing()).to.equal(4); + }); + + it("can work with classes", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.method()).to.equal(4); + }); + + it("can pass parameters to class constructor", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(23); + expect(await instance.counter).to.equal(23); + }); + + it("can access a class in an object", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose({ SampleClass }, this.port2); + const instance = await new thing.SampleClass(); + expect(await instance.method()).to.equal(4); + }); + + it("can work with class instance properties", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance._counter).to.equal(1); + }); + + it("can set class instance properties", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance._counter).to.equal(1); + await (instance._counter = 4); + expect(await instance._counter).to.equal(4); + }); + + it("can work with class instance methods", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.counter).to.equal(1); + await instance.increaseCounter(); + expect(await instance.counter).to.equal(2); + }); + + it("can handle throwing class instance methods", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + return instance + .throwsAnError() + .then(_ => Promise.reject()) + .catch(err => {}); + }); + + it("can work with class instance methods multiple times", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.counter).to.equal(1); + await instance.increaseCounter(); + await instance.increaseCounter(5); + expect(await instance.counter).to.equal(7); + }); + + it("can work with class instance methods that return promises", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.promiseFunc()).to.equal(4); + }); + + it("can work with class instance properties that are promises", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance._promise).to.equal(4); + }); + + it("can work with class instance getters that are promises", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.promise).to.equal(4); + }); + + it("can work with static class properties", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + expect(await thing.SOME_NUMBER).to.equal(4); + }); + + it("can work with static class methods", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + expect(await thing.ADD(1, 3)).to.equal(4); + }); + + it("can work with bound class instance methods", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.counter).to.equal(1); + const method = instance.increaseCounter.bind(instance); + await method(); + expect(await instance.counter).to.equal(2); + }); + + it("can work with class instance getters", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance.counter).to.equal(1); + await instance.increaseCounter(); + expect(await instance.counter).to.equal(2); + }); + + it("can work with class instance setters", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + expect(await instance._counter).to.equal(1); + await (instance.counter = 4); + expect(await instance._counter).to.equal(4); + }); + + const hasBroadcastChannel = _ => "BroadcastChannel" in self; + guardedIt(hasBroadcastChannel)( + "will work with BroadcastChannel", + async function() { + const b1 = new BroadcastChannel("comlink_bc_test"); + const b2 = new BroadcastChannel("comlink_bc_test"); + const thing = Comlink.wrapChain(b1); + Comlink.expose(b => 40 + b, b2); + expect(await thing(2)).to.equal(42); + } + ); + + // Buffer transfers seem to have regressed in Safari 11.1, it’s fixed in 11.2. + const isNotSafari11_1 = _ => + !/11\.1(\.[0-9]+)? Safari/.test(navigator.userAgent); + guardedIt(isNotSafari11_1)("will transfer buffers", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(b => b.byteLength, this.port2); + const buffer = new Uint8Array([1, 2, 3]).buffer; + expect(await thing(Comlink.transfer(buffer, [buffer]))).to.equal(3); + expect(buffer.byteLength).to.equal(0); + }); + + guardedIt(isNotSafari11_1)( + "will transfer deeply nested buffers", + async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(a => a.b.c.d.byteLength, this.port2); + const buffer = new Uint8Array([1, 2, 3]).buffer; + expect( + await thing(Comlink.transfer({ b: { c: { d: buffer } } }, [buffer])) + ).to.equal(3); + expect(buffer.byteLength).to.equal(0); + } + ); + + it("will transfer a message port", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(a => a.postMessage("ohai"), this.port2); + const { port1, port2 } = new MessageChannel(); + await thing(Comlink.transfer(port2, [port2])); + return new Promise(resolve => { + port1.onmessage = event => { + expect(event.data).to.equal("ohai"); + resolve(); + }; + }); + }); + + it("will wrap marked return values", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose( + _ => + Comlink.proxy({ + counter: 0, + inc() { + this.counter += 1; + } + }), + this.port2 + ); + const obj = await thing(); + expect(await obj.counter).to.equal(0); + await obj.inc(); + expect(await obj.counter).to.equal(1); + }); + + it("will wrap marked return values from class instance methods", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(SampleClass, this.port2); + const instance = await new thing(); + const obj = await instance.proxyFunc(); + expect(await obj.counter).to.equal(0); + await obj.inc(); + expect(await obj.counter).to.equal(1); + }); + + it("will wrap marked parameter values", async function() { + const thing = Comlink.wrapChain(this.port1); + const local = { + counter: 0, + inc() { + this.counter++; + } + }; + Comlink.expose(async function(f) { + await f.inc(); + }, this.port2); + expect(local.counter).to.equal(0); + await thing(Comlink.proxy(local)); + expect(await local.counter).to.equal(1); + }); + + it("will wrap marked assignments", async function() { + const thing = Comlink.wrapChain(this.port1); + const obj = { + onready: null, + call() { + return this.onready(); + } + }; + Comlink.expose(obj, this.port2); + + thing.onready = Comlink.proxy(() => 1); + expect(await thing.call()).to.equal(1); + }); + + it("will wrap marked parameter values, simple function", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(async function(f) { + return await f(); + }, this.port2); + expect(await thing(Comlink.proxy(_ => 1))).to.equal(1); + }); + + it("will wrap multiple marked parameter values, simple function", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose(async function(f1, f2, f3) { + return (await f1()) + (await f2()) + (await f3()); + }, this.port2); + // Weird code because Mocha + expect( + await thing( + Comlink.proxy(_ => 1), + Comlink.proxy(_ => 2), + Comlink.proxy(_ => 3) + ) + ).to.equal(6); + }); + + it("will proxy deeply nested values", async function() { + const thing = Comlink.wrapChain(this.port1); + const obj = { + a: { + v: 4 + }, + b: Comlink.proxy({ + v: 5 + }) + }; + Comlink.expose(obj, this.port2); + + const a = await thing.a; + const b = await thing.b; + expect(await a.v).to.equal(4); + expect(await b.v).to.equal(5); + await (a.v = 8); + await (b.v = 9); + expect(await thing.a.v).to.equal(4); + expect(await thing.b.v).to.equal(9); + }); + + it("will handle undefined parameters", async function() { + const thing = Comlink.wrapChain(this.port1); + Comlink.expose({ f: _ => 4 }, this.port2); + expect(await thing.f(undefined)).to.equal(4); + }); + + it("can handle destructuring", async function() { + Comlink.expose( + { + a: 4, + get b() { + return 5; + }, + c() { + return 6; + } + }, + this.port2 + ); + const { a, b, c } = Comlink.wrapChain(this.port1); + expect(await a).to.equal(4); + expect(await b).to.equal(5); + expect(await c()).to.equal(6); + }); + + it("lets users define transfer handlers", function(done) { + Comlink.transferHandlers.set("event", { + canHandle(obj) { + return obj instanceof Event; + }, + serialize(obj) { + return [obj.data, []]; + }, + deserialize(data) { + return new MessageEvent("message", { data }); + } + }); + + Comlink.expose(ev => { + expect(ev).to.be.an.instanceOf(Event); + expect(ev.data).to.deep.equal({ a: 1 }); + done(); + }, this.port1); + const thing = Comlink.wrapChain(this.port2); + + const { port1, port2 } = new MessageChannel(); + port1.addEventListener("message", async (m) => { + await thing(m); + }); + port1.start(); + port2.postMessage({ a: 1 }); + }); + + it("can tunnel a new endpoint with createEndpoint", async function() { + Comlink.expose( + { + a: 4, + c() { + return 5; + } + }, + this.port2 + ); + const proxy = Comlink.wrapChain(this.port1); + const otherEp = await proxy[Comlink.createEndpoint](); + const otherProxy = Comlink.wrapChain(otherEp); + expect(await otherProxy.a).to.equal(4); + expect(await proxy.a).to.equal(4); + expect(await otherProxy.c()).to.equal(5); + expect(await proxy.c()).to.equal(5); + }); + }); +}); + +function guardedIt(f) { + return f() ? it : xit; +}