message-port-rpc
Version:
Turns a MessagePort into an remote procedure call (RPC) stub.
138 lines (131 loc) • 6.58 kB
JavaScript
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