1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | import * as Comlink from "/base/dist/esm/comlink.mjs";
|
15 |
|
16 | class 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 |
|
68 | describe("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 |
|
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 |
|
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 |
|
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 |
|
497 |
|
498 |
|
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 |
|
597 | await new Promise((resolve) => setTimeout(resolve, 1));
|
598 | expect(finalized).to.be.true;
|
599 | });
|
600 |
|
601 |
|
602 |
|
603 |
|
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 |
|
619 | this.timeout(10000);
|
620 |
|
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 |
|
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 |
|
653 | function guardedIt(f) {
|
654 | return f() ? it : xit;
|
655 | }
|