/** * 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"; 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 in the same realm", function () { beforeEach(function () { const { port1, port2 } = new MessageChannel(); port1.start(); port2.start(); this.port1 = port1; this.port2 = port2; }); it("can work with objects", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose({ value: 4 }, this.port2); expect(await thing.value).to.equal(4); }); it("can work with functions on an object", async function () { const thing = Comlink.wrap(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.wrap(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.wrap(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.wrap(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 forward an async function error", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose( { async throwError() { throw new Error("Should have thrown"); }, }, this.port2 ); try { await thing.throwError(); } catch (err) { expect(err.message).to.equal("Should have thrown"); } }); it("can rethrow non-error objects", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose((_) => { throw { test: true }; }, this.port2); try { await thing(); throw "Should have thrown"; } catch (err) { expect(err).to.not.equal("Should have thrown"); expect(err.test).to.equal(true); } }); it("can rethrow scalars", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose((_) => { throw "oops"; }, this.port2); try { await thing(); throw "Should have thrown"; } catch (err) { expect(err).to.not.equal("Should have thrown"); expect(err).to.equal("oops"); expect(typeof err).to.equal("string"); } }); it("can rethrow null", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose((_) => { throw null; }, this.port2); try { await thing(); throw "Should have thrown"; } catch (err) { expect(err).to.not.equal("Should have thrown"); expect(err).to.equal(null); expect(typeof err).to.equal("object"); } }); it("can work with parameterized functions", async function () { const thing = Comlink.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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 copy TypedArrays", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose((b) => b, this.port2); const array = new Uint8Array([1, 2, 3]); const receive = await thing(array); expect(array).to.not.equal(receive); expect(array.byteLength).to.equal(receive.byteLength); expect([...array]).to.deep.equal([...receive]); }); guardedIt(isNotSafari11_1)("will copy nested TypedArrays", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose((b) => b, this.port2); const array = new Uint8Array([1, 2, 3]); const receive = await thing({ v: 1, array, }); expect(array).to.not.equal(receive.array); expect(array.byteLength).to.equal(receive.array.byteLength); expect([...array]).to.deep.equal([...receive.array]); }); guardedIt(isNotSafari11_1)( "will transfer deeply nested buffers", async function () { const thing = Comlink.wrap(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.wrap(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.wrap(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.wrap(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.wrap(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", function (done) { const thing = Comlink.wrap(this.port1); const obj = { onready: null, call() { this.onready(); }, }; Comlink.expose(obj, this.port2); thing.onready = Comlink.proxy(() => done()); thing.call(); }); it("will wrap marked parameter values, simple function", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose(async function (f) { await f(); }, this.port2); // Weird code because Mocha await new Promise(async (resolve) => { thing(Comlink.proxy((_) => resolve())); }); }); it("will wrap multiple marked parameter values, simple function", async function () { const thing = Comlink.wrap(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.wrap(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); // Workaround for a weird scheduling inconsistency in Firefox. // This test failed, but not when run in isolation, and only // in Firefox. I think there might be problem with task ordering. await new Promise((resolve) => setTimeout(resolve, 1)); 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.wrap(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.wrap(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.wrap(this.port2); const { port1, port2 } = new MessageChannel(); port1.addEventListener("message", thing.bind(this)); port1.start(); port2.postMessage({ a: 1 }); }); it("can tunnels a new endpoint with createEndpoint", async function () { Comlink.expose( { a: 4, c() { return 5; }, }, this.port2 ); const proxy = Comlink.wrap(this.port1); const otherEp = await proxy[Comlink.createEndpoint](); const otherProxy = Comlink.wrap(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); }); it("released proxy should no longer be useable and throw an exception", async function () { const thing = Comlink.wrap(this.port1); Comlink.expose(SampleClass, this.port2); const instance = await new thing(); await instance[Comlink.releaseProxy](); expect(() => instance.method()).to.throw(); }); it("released proxy should invoke finalizer", async function () { let finalized = false; Comlink.expose( { a: "thing", [Comlink.finalizer]: () => { finalized = true; }, }, this.port2 ); const instance = Comlink.wrap(this.port1); expect(await instance.a).to.equal("thing"); await instance[Comlink.releaseProxy](); // wait a beat to let the events process await new Promise((resolve) => setTimeout(resolve, 1)); expect(finalized).to.be.true; }); // commented out this test as it could be unreliable in various browsers as // it has to wait for GC to kick in which could happen at any timing // this does seem to work when testing locally it.skip("released proxy via GC should invoke finalizer", async function () { let finalized = false; Comlink.expose( { a: "thing", [Comlink.finalizer]: () => { finalized = true; }, }, this.port2 ); let registry; // set a long enough timeout to wait for a garbage collection this.timeout(10000); // promise will resolve when the proxy is garbage collected await new Promise(async (resolve, reject) => { registry = new FinalizationRegistry((heldValue) => { heldValue(); }); const instance = Comlink.wrap(this.port1); registry.register(instance, resolve); expect(await instance.a).to.equal("thing"); }); // wait a beat to let the events process await new Promise((resolve) => setTimeout(resolve, 1)); expect(finalized).to.be.true; }); it("can proxy with a given target", async function () { const thing = Comlink.wrap(this.port1, { value: {} }); Comlink.expose({ value: 4 }, this.port2); expect(await thing.value).to.equal(4); }); it("can handle unserializable types", async function () { const thing = Comlink.wrap(this.port1, { value: {} }); Comlink.expose({ value: () => "boom" }, this.port2); try { await thing.value; } catch (err) { expect(err.message).to.equal("Unserializable return value"); } }); }); function guardedIt(f) { return f() ? it : xit; }