UNPKG

@cruise-automation/rpc

Version:

Add RPC to WebWorkers with transferrable object support

177 lines (141 loc) 5.01 kB
"use strict"; 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");