@cruise-automation/rpc
Version:
Add RPC to WebWorkers with transferrable object support
177 lines (141 loc) • 5.01 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.createLinkedChannels = createLinkedChannels;
exports.default = void 0;
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
//
// 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.
// this type mirrors the MessageChannel api which is available on
// instances of web-workers as well as avaiable on 'global' within a worker
const RESPONSE = "$$RESPONSE";
const ERROR = "$$ERROR"; // helper function to create linked channels for testing
function createLinkedChannels() {
const local = {
postMessage(data, transfer) {
const ev = new MessageEvent("message", {
data
}); // eslint-disable-next-line no-use-before-define
if (remote.onmessage) {
remote.onmessage(ev); // eslint-disable-line no-use-before-define
}
}
};
const remote = {
postMessage(data, transfer) {
const ev = new MessageEvent("message", {
data
});
if (local.onmessage) {
local.onmessage(ev);
}
}
};
return {
local,
remote
};
} // This class allows you to hook up bi-directional async calls across web-worker
// boundaries where a single call to or from a worker can 'wait' on the response.
// Errors in receivers are propigated back to the caller as a rejection.
// It also supports returning transferables over the web-worker postMessage api,
// which was the main shortcomming with the worker-rpc npm module.
// To attach rpc to an instance of a worker in the main thread:
// const rpc = new Rpc(workerInstace);
// To attach rpc within an a web worker:
// const rpc = new Rpc(global);
// Check out the tests for more examples.
class Rpc {
constructor(channel) {
_defineProperty(this, "_channel", void 0);
_defineProperty(this, "_messageId", 0);
_defineProperty(this, "_pendingCallbacks", {});
_defineProperty(this, "_receivers", new Map());
_defineProperty(this, "_onChannelMessage", ev => {
const {
id,
topic,
data
} = ev.data;
if (topic === RESPONSE) {
this._pendingCallbacks[id](ev.data);
delete this._pendingCallbacks[id];
return;
} // invoke the receive handler in a promise so if it throws synchronously we can reject
new Promise((resolve, reject) => {
const handler = this._receivers.get(topic);
if (!handler) {
throw new Error(`no receiver registered for ${topic}`);
} // This works both when `handler` returns a value or a Promise.
resolve(handler(data));
}).then(result => {
if (!result) {
return this._channel.postMessage({
topic: RESPONSE,
id
});
}
const transferables = result[Rpc.transferables];
delete result[Rpc.transferables];
const message = {
topic: RESPONSE,
id,
data: result
};
this._channel.postMessage(message, transferables);
}).catch(err => {
const message = {
topic: RESPONSE,
id,
data: {
[ERROR]: true,
message: err.message
}
};
this._channel.postMessage(message);
});
});
this._channel = channel;
if (this._channel.onmessage) {
throw new Error("channel.onmessage is already set. Can only use one Rpc instance per channel.");
}
this._channel.onmessage = this._onChannelMessage;
}
// Send a message across the rpc boundary to a receiver on the other side.
// This returns a promise for the receiver's response. If there is no registered
// receiver for the given topic, this method throws.
send(topic, data, transfer) {
const id = this._messageId++;
const message = {
topic,
id,
data
};
const result = new Promise((resolve, reject) => {
this._pendingCallbacks[id] = info => {
if (info.data && info.data[ERROR]) {
reject(new Error(info.data.message));
} else {
resolve(info.data);
}
};
});
this._channel.postMessage(message, transfer);
return result;
} // Register a receiver for a given message on a topic.
// Only one receiver can be registered per topic, and currently
// 'deregistering' a receiver is not supported.
receive(topic, handler) {
if (this._receivers.has(topic)) {
throw new Error(`Receiver already registered for topic: ${topic}`);
}
this._receivers.set(topic, handler);
}
}
exports.default = Rpc;
_defineProperty(Rpc, "transferables", "$$TRANSFERABLES");