UNPKG

message-port-rpc

Version:

Turns a MessagePort into an remote procedure call (RPC) stub.

138 lines (131 loc) 6.58 kB
import _regeneratorRuntime from "@babel/runtime-corejs3/helpers/regeneratorRuntime"; import _toConsumableArray from "@babel/runtime-corejs3/helpers/toConsumableArray"; import _asyncToGenerator from "@babel/runtime-corejs3/helpers/asyncToGenerator"; import _slicedToArray from "@babel/runtime-corejs3/helpers/slicedToArray"; import _Array$isArray from "@babel/runtime-corejs3/core-js-stable/array/is-array"; import _concatInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/concat"; import _sliceInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/slice"; import _Promise from "@babel/runtime-corejs3/core-js-stable/promise"; // Naming is from https://www.w3.org/History/1992/nfs_dxcern_mirror/rpc/doc/Introduction/HowItWorks.html. var ABORT = 'ABORT'; var CALL = 'CALL'; var REJECT = 'REJECT'; var RESOLVE = 'RESOLVE'; // eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any // Regardless whether T returns Promise or not, the client stub must return Promise. /** * Binds a function to a `MessagePort` in RPC fashion and/or create a RPC function stub connected to a `MessagePort`. * * In a traditional RPC setting: * - server should call this function with `fn` argument, the returned function should be ignored; * - client should call this function without `fn` argument, the returned function is the stub to call the server. * * This function supports bidirectional RPC when both sides are passing the `fn` argument. * * When calling the returned function stub, the arguments and return value are transferred over `MessagePort`. * Thus, they will be cloned by the underlying structured clone algorithm. * * The returned stub has a variant `withOptions` for passing transferables and abort signal. * * @param {MessagePort} port - The `MessagePort` object to send the calls. The underlying `MessageChannel` must be exclusively used by this function only. * @param {Function} fn - The function to invoke. If not set, this RPC cannot be invoked by the other side of `MessagePort`. * * @returns A function, when called, will invoke the function on the other side of `MessagePort`. */ export default function messagePortRPC(port, fn) { // We cannot neuter the input port because it would cause memory leak: // - We can neuter a port by passing it through Structured Clone Algorithm so the input port will become non-functional // - After a port is neutered, closing the neutered port will not close the cloned port // - Thus, the port owner will no longer able to close the port // - This defeated our philosophy: whoever pass a resources to a function, should own the resources unless it is intentional and no other workarounds // eslint-disable-next-line @typescript-eslint/no-explicit-any var handleMessage = function handleMessage(event) { var data = event.data; if (_Array$isArray(data) && data[0] === CALL) { event.stopImmediatePropagation(); var _event$ports = _slicedToArray(event.ports, 1), returnPort = _event$ports[0]; if (!returnPort) { throw new Error('RPCCallMessage must contains a port.'); } if (fn) { _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() { var abortController, _context; return _regeneratorRuntime().wrap(function _callee$(_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: abortController = new AbortController(); _context2.prev = 1; returnPort.onmessage = function (_ref2) { var data = _ref2.data; return _Array$isArray(data) && data[0] === ABORT && abortController.abort(); }; _context2.t0 = returnPort; _context2.t1 = RESOLVE; _context2.next = 7; return fn.call.apply(fn, _concatInstanceProperty(_context = [{ signal: abortController.signal }]).call(_context, _toConsumableArray(_sliceInstanceProperty(data).call(data, 1)))); case 7: _context2.t2 = _context2.sent; _context2.t3 = [_context2.t1, _context2.t2]; _context2.t0.postMessage.call(_context2.t0, _context2.t3); _context2.next = 15; break; case 12: _context2.prev = 12; _context2.t4 = _context2["catch"](1); returnPort.postMessage([REJECT, _context2.t4]); case 15: _context2.prev = 15; returnPort.close(); return _context2.finish(15); case 18: case "end": return _context2.stop(); } }, _callee, null, [[1, 12, 15, 18]]); }))(); } else { returnPort.postMessage([REJECT, new Error('No function was registered on this RPC. This is probably calling a client which do not implement the function.')]); returnPort.close(); } } }; port.addEventListener('message', handleMessage); port.start(); var createWithOptions = function createWithOptions(init) { return function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return new _Promise(function (resolve, reject) { var _init$signal, _context3, _context4; var _MessageChannel = new MessageChannel(), port1 = _MessageChannel.port1, port2 = _MessageChannel.port2; port1.onmessage = function (event) { var data = event.data; if (data[0] === RESOLVE) { resolve(data[1]); } else { reject(data[1]); } port1.close(); }; init === null || init === void 0 || (_init$signal = init.signal) === null || _init$signal === void 0 || _init$signal.addEventListener('abort', function () { port1.postMessage([ABORT]); port1.close(); reject(new Error('Aborted.')); }); var callMessage = _concatInstanceProperty(_context3 = [CALL]).call(_context3, args); port.postMessage(callMessage, _concatInstanceProperty(_context4 = [port2]).call(_context4, _toConsumableArray(init.transfer || []))); }); }; }; var stub = createWithOptions({}); stub.withOptions = createWithOptions; return stub; } //# sourceMappingURL=messagePortRPC.js.map