UNPKG

19.7 kBJavaScriptView Raw
1/**
2 * Copyright 2017 Google Inc. All Rights Reserved.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 * http://www.apache.org/licenses/LICENSE-2.0
7 * Unless required by applicable law or agreed to in writing, software
8 * distributed under the License is distributed on an "AS IS" BASIS,
9 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 * See the License for the specific language governing permissions and
11 * limitations under the License.
12 */
13
14import * as Comlink from "/base/dist/esm/comlink.mjs";
15
16class SampleClass {
17 constructor(counterInit = 1) {
18 this._counter = counterInit;
19 this._promise = Promise.resolve(4);
20 }
21
22 static get SOME_NUMBER() {
23 return 4;
24 }
25
26 static ADD(a, b) {
27 return a + b;
28 }
29
30 get counter() {
31 return this._counter;
32 }
33
34 set counter(value) {
35 this._counter = value;
36 }
37
38 get promise() {
39 return this._promise;
40 }
41
42 method() {
43 return 4;
44 }
45
46 increaseCounter(delta = 1) {
47 this._counter += delta;
48 }
49
50 promiseFunc() {
51 return new Promise((resolve) => setTimeout((_) => resolve(4), 100));
52 }
53
54 proxyFunc() {
55 return Comlink.proxy({
56 counter: 0,
57 inc() {
58 this.counter++;
59 },
60 });
61 }
62
63 throwsAnError() {
64 throw Error("OMG");
65 }
66}
67
68describe("Comlink in the same realm", function () {
69 beforeEach(function () {
70 const { port1, port2 } = new MessageChannel();
71 port1.start();
72 port2.start();
73 this.port1 = port1;
74 this.port2 = port2;
75 });
76
77 it("can work with objects", async function () {
78 const thing = Comlink.wrap(this.port1);
79 Comlink.expose({ value: 4 }, this.port2);
80 expect(await thing.value).to.equal(4);
81 });
82
83 it("can work with functions on an object", async function () {
84 const thing = Comlink.wrap(this.port1);
85 Comlink.expose({ f: (_) => 4 }, this.port2);
86 expect(await thing.f()).to.equal(4);
87 });
88
89 it("can work with functions", async function () {
90 const thing = Comlink.wrap(this.port1);
91 Comlink.expose((_) => 4, this.port2);
92 expect(await thing()).to.equal(4);
93 });
94
95 it("can work with objects that have undefined properties", async function () {
96 const thing = Comlink.wrap(this.port1);
97 Comlink.expose({ x: undefined }, this.port2);
98 expect(await thing.x).to.be.undefined;
99 });
100
101 it("can keep the stack and message of thrown errors", async function () {
102 let stack;
103 const thing = Comlink.wrap(this.port1);
104 Comlink.expose((_) => {
105 const error = Error("OMG");
106 stack = error.stack;
107 throw error;
108 }, this.port2);
109 try {
110 await thing();
111 throw "Should have thrown";
112 } catch (err) {
113 expect(err).to.not.eq("Should have thrown");
114 expect(err.message).to.equal("OMG");
115 expect(err.stack).to.equal(stack);
116 }
117 });
118
119 it("can forward an async function error", async function () {
120 const thing = Comlink.wrap(this.port1);
121 Comlink.expose(
122 {
123 async throwError() {
124 throw new Error("Should have thrown");
125 },
126 },
127 this.port2
128 );
129 try {
130 await thing.throwError();
131 } catch (err) {
132 expect(err.message).to.equal("Should have thrown");
133 }
134 });
135
136 it("can rethrow non-error objects", async function () {
137 const thing = Comlink.wrap(this.port1);
138 Comlink.expose((_) => {
139 throw { test: true };
140 }, this.port2);
141 try {
142 await thing();
143 throw "Should have thrown";
144 } catch (err) {
145 expect(err).to.not.equal("Should have thrown");
146 expect(err.test).to.equal(true);
147 }
148 });
149
150 it("can rethrow scalars", async function () {
151 const thing = Comlink.wrap(this.port1);
152 Comlink.expose((_) => {
153 throw "oops";
154 }, this.port2);
155 try {
156 await thing();
157 throw "Should have thrown";
158 } catch (err) {
159 expect(err).to.not.equal("Should have thrown");
160 expect(err).to.equal("oops");
161 expect(typeof err).to.equal("string");
162 }
163 });
164
165 it("can rethrow null", async function () {
166 const thing = Comlink.wrap(this.port1);
167 Comlink.expose((_) => {
168 throw null;
169 }, this.port2);
170 try {
171 await thing();
172 throw "Should have thrown";
173 } catch (err) {
174 expect(err).to.not.equal("Should have thrown");
175 expect(err).to.equal(null);
176 expect(typeof err).to.equal("object");
177 }
178 });
179
180 it("can work with parameterized functions", async function () {
181 const thing = Comlink.wrap(this.port1);
182 Comlink.expose((a, b) => a + b, this.port2);
183 expect(await thing(1, 3)).to.equal(4);
184 });
185
186 it("can work with functions that return promises", async function () {
187 const thing = Comlink.wrap(this.port1);
188 Comlink.expose(
189 (_) => new Promise((resolve) => setTimeout((_) => resolve(4), 100)),
190 this.port2
191 );
192 expect(await thing()).to.equal(4);
193 });
194
195 it("can work with classes", async function () {
196 const thing = Comlink.wrap(this.port1);
197 Comlink.expose(SampleClass, this.port2);
198 const instance = await new thing();
199 expect(await instance.method()).to.equal(4);
200 });
201
202 it("can pass parameters to class constructor", async function () {
203 const thing = Comlink.wrap(this.port1);
204 Comlink.expose(SampleClass, this.port2);
205 const instance = await new thing(23);
206 expect(await instance.counter).to.equal(23);
207 });
208
209 it("can access a class in an object", async function () {
210 const thing = Comlink.wrap(this.port1);
211 Comlink.expose({ SampleClass }, this.port2);
212 const instance = await new thing.SampleClass();
213 expect(await instance.method()).to.equal(4);
214 });
215
216 it("can work with class instance properties", async function () {
217 const thing = Comlink.wrap(this.port1);
218 Comlink.expose(SampleClass, this.port2);
219 const instance = await new thing();
220 expect(await instance._counter).to.equal(1);
221 });
222
223 it("can set class instance properties", async function () {
224 const thing = Comlink.wrap(this.port1);
225 Comlink.expose(SampleClass, this.port2);
226 const instance = await new thing();
227 expect(await instance._counter).to.equal(1);
228 await (instance._counter = 4);
229 expect(await instance._counter).to.equal(4);
230 });
231
232 it("can work with class instance methods", async function () {
233 const thing = Comlink.wrap(this.port1);
234 Comlink.expose(SampleClass, this.port2);
235 const instance = await new thing();
236 expect(await instance.counter).to.equal(1);
237 await instance.increaseCounter();
238 expect(await instance.counter).to.equal(2);
239 });
240
241 it("can handle throwing class instance methods", async function () {
242 const thing = Comlink.wrap(this.port1);
243 Comlink.expose(SampleClass, this.port2);
244 const instance = await new thing();
245 return instance
246 .throwsAnError()
247 .then((_) => Promise.reject())
248 .catch((err) => {});
249 });
250
251 it("can work with class instance methods multiple times", async function () {
252 const thing = Comlink.wrap(this.port1);
253 Comlink.expose(SampleClass, this.port2);
254 const instance = await new thing();
255 expect(await instance.counter).to.equal(1);
256 await instance.increaseCounter();
257 await instance.increaseCounter(5);
258 expect(await instance.counter).to.equal(7);
259 });
260
261 it("can work with class instance methods that return promises", async function () {
262 const thing = Comlink.wrap(this.port1);
263 Comlink.expose(SampleClass, this.port2);
264 const instance = await new thing();
265 expect(await instance.promiseFunc()).to.equal(4);
266 });
267
268 it("can work with class instance properties that are promises", async function () {
269 const thing = Comlink.wrap(this.port1);
270 Comlink.expose(SampleClass, this.port2);
271 const instance = await new thing();
272 expect(await instance._promise).to.equal(4);
273 });
274
275 it("can work with class instance getters that are promises", async function () {
276 const thing = Comlink.wrap(this.port1);
277 Comlink.expose(SampleClass, this.port2);
278 const instance = await new thing();
279 expect(await instance.promise).to.equal(4);
280 });
281
282 it("can work with static class properties", async function () {
283 const thing = Comlink.wrap(this.port1);
284 Comlink.expose(SampleClass, this.port2);
285 expect(await thing.SOME_NUMBER).to.equal(4);
286 });
287
288 it("can work with static class methods", async function () {
289 const thing = Comlink.wrap(this.port1);
290 Comlink.expose(SampleClass, this.port2);
291 expect(await thing.ADD(1, 3)).to.equal(4);
292 });
293
294 it("can work with bound class instance methods", async function () {
295 const thing = Comlink.wrap(this.port1);
296 Comlink.expose(SampleClass, this.port2);
297 const instance = await new thing();
298 expect(await instance.counter).to.equal(1);
299 const method = instance.increaseCounter.bind(instance);
300 await method();
301 expect(await instance.counter).to.equal(2);
302 });
303
304 it("can work with class instance getters", async function () {
305 const thing = Comlink.wrap(this.port1);
306 Comlink.expose(SampleClass, this.port2);
307 const instance = await new thing();
308 expect(await instance.counter).to.equal(1);
309 await instance.increaseCounter();
310 expect(await instance.counter).to.equal(2);
311 });
312
313 it("can work with class instance setters", async function () {
314 const thing = Comlink.wrap(this.port1);
315 Comlink.expose(SampleClass, this.port2);
316 const instance = await new thing();
317 expect(await instance._counter).to.equal(1);
318 await (instance.counter = 4);
319 expect(await instance._counter).to.equal(4);
320 });
321
322 const hasBroadcastChannel = (_) => "BroadcastChannel" in self;
323 guardedIt(hasBroadcastChannel)(
324 "will work with BroadcastChannel",
325 async function () {
326 const b1 = new BroadcastChannel("comlink_bc_test");
327 const b2 = new BroadcastChannel("comlink_bc_test");
328 const thing = Comlink.wrap(b1);
329 Comlink.expose((b) => 40 + b, b2);
330 expect(await thing(2)).to.equal(42);
331 }
332 );
333
334 // Buffer transfers seem to have regressed in Safari 11.1, it’s fixed in 11.2.
335 const isNotSafari11_1 = (_) =>
336 !/11\.1(\.[0-9]+)? Safari/.test(navigator.userAgent);
337 guardedIt(isNotSafari11_1)("will transfer buffers", async function () {
338 const thing = Comlink.wrap(this.port1);
339 Comlink.expose((b) => b.byteLength, this.port2);
340 const buffer = new Uint8Array([1, 2, 3]).buffer;
341 expect(await thing(Comlink.transfer(buffer, [buffer]))).to.equal(3);
342 expect(buffer.byteLength).to.equal(0);
343 });
344
345 guardedIt(isNotSafari11_1)("will copy TypedArrays", async function () {
346 const thing = Comlink.wrap(this.port1);
347 Comlink.expose((b) => b, this.port2);
348 const array = new Uint8Array([1, 2, 3]);
349 const receive = await thing(array);
350 expect(array).to.not.equal(receive);
351 expect(array.byteLength).to.equal(receive.byteLength);
352 expect([...array]).to.deep.equal([...receive]);
353 });
354
355 guardedIt(isNotSafari11_1)("will copy nested TypedArrays", async function () {
356 const thing = Comlink.wrap(this.port1);
357 Comlink.expose((b) => b, this.port2);
358 const array = new Uint8Array([1, 2, 3]);
359 const receive = await thing({
360 v: 1,
361 array,
362 });
363 expect(array).to.not.equal(receive.array);
364 expect(array.byteLength).to.equal(receive.array.byteLength);
365 expect([...array]).to.deep.equal([...receive.array]);
366 });
367
368 guardedIt(isNotSafari11_1)(
369 "will transfer deeply nested buffers",
370 async function () {
371 const thing = Comlink.wrap(this.port1);
372 Comlink.expose((a) => a.b.c.d.byteLength, this.port2);
373 const buffer = new Uint8Array([1, 2, 3]).buffer;
374 expect(
375 await thing(Comlink.transfer({ b: { c: { d: buffer } } }, [buffer]))
376 ).to.equal(3);
377 expect(buffer.byteLength).to.equal(0);
378 }
379 );
380
381 it("will transfer a message port", async function () {
382 const thing = Comlink.wrap(this.port1);
383 Comlink.expose((a) => a.postMessage("ohai"), this.port2);
384 const { port1, port2 } = new MessageChannel();
385 await thing(Comlink.transfer(port2, [port2]));
386 return new Promise((resolve) => {
387 port1.onmessage = (event) => {
388 expect(event.data).to.equal("ohai");
389 resolve();
390 };
391 });
392 });
393
394 it("will wrap marked return values", async function () {
395 const thing = Comlink.wrap(this.port1);
396 Comlink.expose(
397 (_) =>
398 Comlink.proxy({
399 counter: 0,
400 inc() {
401 this.counter += 1;
402 },
403 }),
404 this.port2
405 );
406 const obj = await thing();
407 expect(await obj.counter).to.equal(0);
408 await obj.inc();
409 expect(await obj.counter).to.equal(1);
410 });
411
412 it("will wrap marked return values from class instance methods", async function () {
413 const thing = Comlink.wrap(this.port1);
414 Comlink.expose(SampleClass, this.port2);
415 const instance = await new thing();
416 const obj = await instance.proxyFunc();
417 expect(await obj.counter).to.equal(0);
418 await obj.inc();
419 expect(await obj.counter).to.equal(1);
420 });
421
422 it("will wrap marked parameter values", async function () {
423 const thing = Comlink.wrap(this.port1);
424 const local = {
425 counter: 0,
426 inc() {
427 this.counter++;
428 },
429 };
430 Comlink.expose(async function (f) {
431 await f.inc();
432 }, this.port2);
433 expect(local.counter).to.equal(0);
434 await thing(Comlink.proxy(local));
435 expect(await local.counter).to.equal(1);
436 });
437
438 it("will wrap marked assignments", function (done) {
439 const thing = Comlink.wrap(this.port1);
440 const obj = {
441 onready: null,
442 call() {
443 this.onready();
444 },
445 };
446 Comlink.expose(obj, this.port2);
447
448 thing.onready = Comlink.proxy(() => done());
449 thing.call();
450 });
451
452 it("will wrap marked parameter values, simple function", async function () {
453 const thing = Comlink.wrap(this.port1);
454 Comlink.expose(async function (f) {
455 await f();
456 }, this.port2);
457 // Weird code because Mocha
458 await new Promise(async (resolve) => {
459 thing(Comlink.proxy((_) => resolve()));
460 });
461 });
462
463 it("will wrap multiple marked parameter values, simple function", async function () {
464 const thing = Comlink.wrap(this.port1);
465 Comlink.expose(async function (f1, f2, f3) {
466 return (await f1()) + (await f2()) + (await f3());
467 }, this.port2);
468 // Weird code because Mocha
469 expect(
470 await thing(
471 Comlink.proxy((_) => 1),
472 Comlink.proxy((_) => 2),
473 Comlink.proxy((_) => 3)
474 )
475 ).to.equal(6);
476 });
477
478 it("will proxy deeply nested values", async function () {
479 const thing = Comlink.wrap(this.port1);
480 const obj = {
481 a: {
482 v: 4,
483 },
484 b: Comlink.proxy({
485 v: 5,
486 }),
487 };
488 Comlink.expose(obj, this.port2);
489
490 const a = await thing.a;
491 const b = await thing.b;
492 expect(await a.v).to.equal(4);
493 expect(await b.v).to.equal(5);
494 await (a.v = 8);
495 await (b.v = 9);
496 // Workaround for a weird scheduling inconsistency in Firefox.
497 // This test failed, but not when run in isolation, and only
498 // in Firefox. I think there might be problem with task ordering.
499 await new Promise((resolve) => setTimeout(resolve, 1));
500 expect(await thing.a.v).to.equal(4);
501 expect(await thing.b.v).to.equal(9);
502 });
503
504 it("will handle undefined parameters", async function () {
505 const thing = Comlink.wrap(this.port1);
506 Comlink.expose({ f: (_) => 4 }, this.port2);
507 expect(await thing.f(undefined)).to.equal(4);
508 });
509
510 it("can handle destructuring", async function () {
511 Comlink.expose(
512 {
513 a: 4,
514 get b() {
515 return 5;
516 },
517 c() {
518 return 6;
519 },
520 },
521 this.port2
522 );
523 const { a, b, c } = Comlink.wrap(this.port1);
524 expect(await a).to.equal(4);
525 expect(await b).to.equal(5);
526 expect(await c()).to.equal(6);
527 });
528
529 it("lets users define transfer handlers", function (done) {
530 Comlink.transferHandlers.set("event", {
531 canHandle(obj) {
532 return obj instanceof Event;
533 },
534 serialize(obj) {
535 return [obj.data, []];
536 },
537 deserialize(data) {
538 return new MessageEvent("message", { data });
539 },
540 });
541
542 Comlink.expose((ev) => {
543 expect(ev).to.be.an.instanceOf(Event);
544 expect(ev.data).to.deep.equal({ a: 1 });
545 done();
546 }, this.port1);
547 const thing = Comlink.wrap(this.port2);
548
549 const { port1, port2 } = new MessageChannel();
550 port1.addEventListener("message", thing.bind(this));
551 port1.start();
552 port2.postMessage({ a: 1 });
553 });
554
555 it("can tunnels a new endpoint with createEndpoint", async function () {
556 Comlink.expose(
557 {
558 a: 4,
559 c() {
560 return 5;
561 },
562 },
563 this.port2
564 );
565 const proxy = Comlink.wrap(this.port1);
566 const otherEp = await proxy[Comlink.createEndpoint]();
567 const otherProxy = Comlink.wrap(otherEp);
568 expect(await otherProxy.a).to.equal(4);
569 expect(await proxy.a).to.equal(4);
570 expect(await otherProxy.c()).to.equal(5);
571 expect(await proxy.c()).to.equal(5);
572 });
573
574 it("released proxy should no longer be useable and throw an exception", async function () {
575 const thing = Comlink.wrap(this.port1);
576 Comlink.expose(SampleClass, this.port2);
577 const instance = await new thing();
578 await instance[Comlink.releaseProxy]();
579 expect(() => instance.method()).to.throw();
580 });
581
582 it("released proxy should invoke finalizer", async function () {
583 let finalized = false;
584 Comlink.expose(
585 {
586 a: "thing",
587 [Comlink.finalizer]: () => {
588 finalized = true;
589 },
590 },
591 this.port2
592 );
593 const instance = Comlink.wrap(this.port1);
594 expect(await instance.a).to.equal("thing");
595 await instance[Comlink.releaseProxy]();
596 // wait a beat to let the events process
597 await new Promise((resolve) => setTimeout(resolve, 1));
598 expect(finalized).to.be.true;
599 });
600
601 // commented out this test as it could be unreliable in various browsers as
602 // it has to wait for GC to kick in which could happen at any timing
603 // this does seem to work when testing locally
604 it.skip("released proxy via GC should invoke finalizer", async function () {
605 let finalized = false;
606 Comlink.expose(
607 {
608 a: "thing",
609 [Comlink.finalizer]: () => {
610 finalized = true;
611 },
612 },
613 this.port2
614 );
615
616 let registry;
617
618 // set a long enough timeout to wait for a garbage collection
619 this.timeout(10000);
620 // promise will resolve when the proxy is garbage collected
621 await new Promise(async (resolve, reject) => {
622 registry = new FinalizationRegistry((heldValue) => {
623 heldValue();
624 });
625
626 const instance = Comlink.wrap(this.port1);
627 registry.register(instance, resolve);
628 expect(await instance.a).to.equal("thing");
629 });
630 // wait a beat to let the events process
631 await new Promise((resolve) => setTimeout(resolve, 1));
632 expect(finalized).to.be.true;
633 });
634
635 it("can proxy with a given target", async function () {
636 const thing = Comlink.wrap(this.port1, { value: {} });
637 Comlink.expose({ value: 4 }, this.port2);
638 expect(await thing.value).to.equal(4);
639 });
640
641 it("can handle unserializable types", async function () {
642 const thing = Comlink.wrap(this.port1, { value: {} });
643 Comlink.expose({ value: () => "boom" }, this.port2);
644
645 try {
646 await thing.value;
647 } catch (err) {
648 expect(err.message).to.equal("Unserializable return value");
649 }
650 });
651});
652
653function guardedIt(f) {
654 return f() ? it : xit;
655}