@cruise-automation/rpc
Version:
Add RPC to WebWorkers with transferrable object support
172 lines (156 loc) • 6.11 kB
JavaScript
// @flow
//
// Copyright (c) 2018-present, GM Cruise LLC
//
// This source code is licensed under the Apache License, Version 2.0,
// found in the LICENSE file in the root directory of this source tree.
// You may not use this file except in compliance with the License.
import Rpc, { type Channel, createLinkedChannels } from ".";
const delay = (time) => new Promise((resolve) => setTimeout(resolve, time));
describe("rpc", () => {
it("only allows setting Rpc once per channel", () => {
const { local: mainChannel } = createLinkedChannels();
new Rpc(mainChannel);
expect(() => new Rpc(mainChannel)).toThrow();
});
it("can send and receive", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", (msg) => {
return { bar: msg.foo };
});
const result = await local.send("foo", { foo: "baz" });
expect(result).toEqual({ bar: "baz" });
});
it("can send and receive with a promise", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", async (msg) => {
await new Promise((resolve) => setTimeout(resolve, 10));
return { bar: msg.foo };
});
const result = await local.send("foo", { foo: "baz" });
expect(result).toEqual({ bar: "baz" });
});
it("rejects on send if receive rejects async", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", async (msg) => {
await new Promise((resolve) => setTimeout(resolve, 10));
throw new Error("boom");
});
return local
.send("foo", { foo: "baz" })
.then(() => {
throw new Error("Send should have rejected");
})
.catch((err) => {
expect(err.message).toEqual("boom");
});
});
it("rejects on send if receive rejects sync", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", (msg) => {
throw new Error("boom");
});
return local
.send("foo", { foo: "baz" })
.then(() => {
throw new Error("Send should have rejected");
})
.catch((err) => {
expect(err.message).toEqual("boom");
});
});
it("rejects on send if there is no receiver", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
new Rpc(workerChannel);
return local
.send("foo", { foo: "baz" })
.then(() => {
throw new Error("Send should have rejected");
})
.catch((err) => {
expect(err.message).toContain("no receiver");
});
});
it("can send and receive transferrables", async () => {
const expectedTransfer = new ArrayBuffer(1);
const mainChannel: Channel = {
postMessage(data: any, transfer?: ArrayBuffer[]) {
const ev = new MessageEvent("message", { data });
// eslint-disable-next-line no-use-before-define
if (workerChannel.onmessage) {
workerChannel.onmessage(ev); // eslint-disable-line no-use-before-define
}
},
};
const workerChannel: Channel = {
postMessage(data: any, transfer?: ArrayBuffer[]) {
const ev = new MessageEvent("message", { data });
expect(transfer).toHaveLength(1);
// $FlowFixMe - flow doesn't understand the assertion above
expect(transfer[0]).toBe(expectedTransfer);
if (mainChannel.onmessage) {
mainChannel.onmessage(ev);
}
},
};
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", async (msg) => {
await new Promise((resolve) => setTimeout(resolve, 10));
return {
bar: msg.foo,
[Rpc.transferables]: [expectedTransfer],
};
});
const result = await local.send("foo", { foo: "baz" });
expect(result).toEqual({ bar: "baz" });
});
it("can resolve when receiver returns undefined", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", () => {});
expect(await local.send("foo")).toBeUndefined();
});
it("can resolve multiple operations", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", async (count) => {
await delay(10);
return count;
});
const one = local.send("foo", 1);
const two = local.send("foo", 2);
const three = local.send("foo", 3);
const result = await Promise.all([one, two, three]);
expect(result).toEqual([1, 2, 3]);
});
it("throws when registering a receiver twice", async () => {
const rpc = new Rpc(createLinkedChannels().local);
rpc.receive("foo", () => {});
expect(() => rpc.receive("foo", () => {})).toThrow();
});
// Regression test for memory leak.
it("clears out _pendingCallbacks when done", async () => {
const { local: mainChannel, remote: workerChannel } = createLinkedChannels();
const local = new Rpc(mainChannel);
const worker = new Rpc(workerChannel);
worker.receive("foo", (msg) => {
return { bar: msg.foo };
});
expect(Object.keys(local._pendingCallbacks).length).toEqual(0); // eslint-disable-line no-underscore-dangle
const result = await local.send("foo", { foo: "baz" });
expect(Object.keys(local._pendingCallbacks).length).toEqual(0); // eslint-disable-line no-underscore-dangle
expect(result).toEqual({ bar: "baz" });
});
});