message-port-rpc
Version:
Turns a MessagePort into an remote procedure call (RPC) stub.
146 lines (138 loc) • 7.17 kB
JavaScript
;
var _Object$defineProperty = require("@babel/runtime-corejs3/core-js-stable/object/define-property");
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default;
_Object$defineProperty(exports, "__esModule", {
value: true
});
exports.default = messagePortRPC;
var _regeneratorRuntime2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/regeneratorRuntime"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/toConsumableArray"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/asyncToGenerator"));
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/slicedToArray"));
var _isArray = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/array/is-array"));
var _concat = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/concat"));
var _slice = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/instance/slice"));
var _promise = _interopRequireDefault(require("@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`.
*/
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 ((0, _isArray.default)(data) && data[0] === CALL) {
event.stopImmediatePropagation();
var _event$ports = (0, _slicedToArray2.default)(event.ports, 1),
returnPort = _event$ports[0];
if (!returnPort) {
throw new Error('RPCCallMessage must contains a port.');
}
if (fn) {
(0, _asyncToGenerator2.default)( /*#__PURE__*/(0, _regeneratorRuntime2.default)().mark(function _callee() {
var abortController, _context;
return (0, _regeneratorRuntime2.default)().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 (0, _isArray.default)(data) && data[0] === ABORT && abortController.abort();
};
_context2.t0 = returnPort;
_context2.t1 = RESOLVE;
_context2.next = 7;
return fn.call.apply(fn, (0, _concat.default)(_context = [{
signal: abortController.signal
}]).call(_context, (0, _toConsumableArray2.default)((0, _slice.default)(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.default(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 = (0, _concat.default)(_context3 = [CALL]).call(_context3, args);
port.postMessage(callMessage, (0, _concat.default)(_context4 = [port2]).call(_context4, (0, _toConsumableArray2.default)(init.transfer || [])));
});
};
};
var stub = createWithOptions({});
stub.withOptions = createWithOptions;
return stub;
}
//# sourceMappingURL=messagePortRPC.js.map