UNPKG

reactant-share

Version:

A framework for building shared web applications with Reactant

430 lines (427 loc) 18.5 kB
import { __values, __awaiter, __assign, __decorate, __param, __metadata, __generator } from '../node_modules/tslib/tslib.es6.js'; import { identifierKey, storeKey, injectable, inject, actionIdentifier } from 'reactant'; import { LastAction } from 'reactant-last-action'; import { syncToClientsName, SharedAppOptions, syncClientIdToServerName, syncClientIdsFromClientsName, removeClientIdToServerName, loadFullStateActionName } from '../constants.js'; import { createId } from '../utils.js'; /** * Port Detector * * It provides port detection and client/server port switching functions. */ var PortDetector = /** @class */ (function () { function PortDetector(sharedAppOptions, lastAction) { var _this = this; this.sharedAppOptions = sharedAppOptions; this.lastAction = lastAction; this.serverCallbacks = new Set(); this.clientCallbacks = new Set(); this.clientDestroyCallbacks = new Set(); /** * client id, it will be generated when the port is client, it is null in server port. */ this.clientId = null; /** * allow Disable Sync */ this.allowDisableSync = function () { return true; }; /** * client ids, it will collect all the client ids when the port is server, it is an empty array in client port. */ this.clientIds = []; /** * server hooks for delegate(this, key, args, { _extra: { serverHook: '$hookName' } }) method */ this.serverHooks = {}; this.isolatedModules = []; /** * onServer * * When the port is server, this hook will execute. * And allow to return a function that will be executed when the current port is switched to client. */ this.onServer = function (callback) { if (typeof callback !== 'function') { throw new Error("'onServer' argument should be a function."); } _this.serverCallbacks.add(callback); if (_this.lastHooks && _this.lastHooks.size > 0 && _this.isServer && _this.transport) { try { var hook = callback(_this.transport); _this.lastHooks.add(hook); } catch (e) { console.error(e); } } return function () { _this.serverCallbacks.delete(callback); }; }; /** * onClient * * When the port is client, this hook will execute. * And allow to return a function that will be executed when the current port is switched to server. */ this.onClient = function (callback) { if (typeof callback !== 'function') { throw new Error("'onClient' argument should be a function."); } _this.clientCallbacks.add(callback); if (_this.lastHooks && _this.lastHooks.size > 0 && _this.isClient && _this.transport) { try { var hook = callback(_this.transport); _this.lastHooks.add(hook); } catch (e) { console.error(e); } } return function () { _this.clientCallbacks.delete(callback); }; }; /** * emit client destroy event with clientId */ this.onClientDestroy = function (callback) { if (typeof callback !== 'function') { throw new Error("'onClientDestroy' argument should be a function."); } _this.clientDestroyCallbacks.add(callback); return function () { _this.clientDestroyCallbacks.delete(callback); }; }; this.onClient(function (transport) { _this.clientId = createId(); _this.clientIds = []; _this.syncFullState({ forceSync: false }); var disposeSyncToClients = transport.listen(syncToClientsName, function (fullState) { return __awaiter(_this, void 0, void 0, function () { var store; return __generator(this, function (_a) { if (!fullState) return [2 /*return*/]; store = this[storeKey]; store.dispatch({ type: "".concat(actionIdentifier, "_").concat(loadFullStateActionName), state: this.getNextState(fullState), _reactant: actionIdentifier, }); this.lastAction.sequence = fullState[this.lastAction.stateKey]._sequence; return [2 /*return*/]; }); }); }); transport.emit({ name: syncClientIdToServerName, respond: false }, _this.clientId); var disposeSyncClientIds = transport.listen(syncClientIdsFromClientsName, function () { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (this.clientId) { // for all clients send current client id to server transport.emit({ name: syncClientIdToServerName, respond: false }, this.clientId); } return [2 /*return*/]; }); }); }); var removeClientIdToServer = function () { transport.emit({ name: removeClientIdToServerName, respond: false }, _this.clientId); }; // do not use `unload` event // https://developer.chrome.com/docs/web-platform/deprecating-unload // the pagehide event is just only triggered in shared worker mode window.addEventListener('pagehide', removeClientIdToServer); return function () { _this.previousPort = 'client'; disposeSyncToClients === null || disposeSyncToClients === void 0 ? void 0 : disposeSyncToClients(); disposeSyncClientIds === null || disposeSyncClientIds === void 0 ? void 0 : disposeSyncClientIds(); window.removeEventListener('pagehide', removeClientIdToServer); }; }); this.onServer(function (transport) { _this.clientId = null; transport.emit({ name: syncClientIdsFromClientsName, respond: false }); var disposeSyncClientId = transport.listen(syncClientIdToServerName, function (clientId) { if (!_this.clientIds.includes(clientId)) { _this.clientIds.push(clientId); } }); var disposeRemoveClientId = transport.listen(removeClientIdToServerName, function (clientId) { var e_1, _a; var index = _this.clientIds.findIndex(function (id) { return id === clientId; }); if (index !== -1) { _this.clientIds.splice(index, 1); var callbacks = _this.clientDestroyCallbacks; try { for (var callbacks_1 = __values(callbacks), callbacks_1_1 = callbacks_1.next(); !callbacks_1_1.done; callbacks_1_1 = callbacks_1.next()) { var callback = callbacks_1_1.value; try { callback(clientId); } catch (e) { console.error(e); } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (callbacks_1_1 && !callbacks_1_1.done && (_a = callbacks_1.return)) _a.call(callbacks_1); } finally { if (e_1) throw e_1.error; } } } }); return function () { _this.previousPort = 'server'; disposeSyncClientId === null || disposeSyncClientId === void 0 ? void 0 : disposeSyncClientId(); disposeRemoveClientId === null || disposeRemoveClientId === void 0 ? void 0 : disposeRemoveClientId(); }; }); } /** * all isolated instances state will not be sync to other clients or server. */ PortDetector.prototype.disableShare = function (instance) { if (process.env.NODE_ENV !== 'production') { if (!this.shared) { console.warn("The app is not shared, so it cannot be isolated."); } if (this.isolatedModules.includes(instance)) { console.warn("This module \"".concat(instance.constructor.name, "\" has been disabled for state sharing.")); } } this.isolatedModules = this.isolatedModules.concat(instance); }; Object.defineProperty(PortDetector.prototype, "isolatedInstanceKeys", { get: function () { var _a; if (this.lastIsolatedInstances !== this.isolatedModules) { this.lastIsolatedInstanceKeys = this.isolatedModules.map(function (instance) { return instance[identifierKey]; }); } return (_a = this.lastIsolatedInstanceKeys) !== null && _a !== void 0 ? _a : []; }, enumerable: false, configurable: true }); PortDetector.prototype.hasIsolatedState = function (key) { return this.isolatedInstanceKeys.includes(key); }; Object.defineProperty(PortDetector.prototype, "id", { get: function () { var _a; return (_a = this.clientId) !== null && _a !== void 0 ? _a : '__SERVER__'; }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "shared", { get: function () { return !!(this.sharedAppOptions.port && this.sharedAppOptions.type); }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "name", { get: function () { var _a; return (_a = this.sharedAppOptions.portName) !== null && _a !== void 0 ? _a : 'default'; }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "disableSyncClient", { get: function () { return (document.visibilityState === 'hidden' && !this.sharedAppOptions.forcedSyncClient && this.allowDisableSync()); }, enumerable: false, configurable: true }); PortDetector.prototype.detectPort = function (port) { var _a; return (_a = this.portApp) === null || _a === void 0 ? void 0 : _a[port]; }; Object.defineProperty(PortDetector.prototype, "isWorkerMode", { get: function () { return this.sharedAppOptions.type === 'SharedWorker'; }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "isServerWorker", { get: function () { return this.isWorkerMode && this.isServer; }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "isServer", { get: function () { return !!this.detectPort('server'); }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "isClient", { get: function () { return !!this.detectPort('client'); }, enumerable: false, configurable: true }); Object.defineProperty(PortDetector.prototype, "transports", { get: function () { var _a; return (_a = this.sharedAppOptions.transports) !== null && _a !== void 0 ? _a : {}; }, enumerable: false, configurable: true }); PortDetector.prototype.setPort = function (currentPortApp, transport) { var e_2, _a, e_3, _b; this.transport = transport; if (this.lastHooks) { try { for (var _c = __values(this.lastHooks), _d = _c.next(); !_d.done; _d = _c.next()) { var hook = _d.value; try { hook === null || hook === void 0 ? void 0 : hook(); } catch (e) { console.error(e); } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_d && !_d.done && (_a = _c.return)) _a.call(_c); } finally { if (e_2) throw e_2.error; } } } this.lastHooks = new Set(); this.portApp = currentPortApp; var callbacks = this.isClient ? this.clientCallbacks : this.serverCallbacks; try { for (var callbacks_2 = __values(callbacks), callbacks_2_1 = callbacks_2.next(); !callbacks_2_1.done; callbacks_2_1 = callbacks_2.next()) { var callback = callbacks_2_1.value; try { var hook = callback(transport); this.lastHooks.add(hook); } catch (e) { console.error(e); } } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (callbacks_2_1 && !callbacks_2_1.done && (_b = callbacks_2.return)) _b.call(callbacks_2); } finally { if (e_3) throw e_3.error; } } }; PortDetector.prototype.syncToClients = function () { var _a; var store = this[storeKey]; if (this.transports.server) { (_a = this.transports.server) === null || _a === void 0 ? void 0 : _a.emit({ name: syncToClientsName, respond: false }, store.getState()); } else { throw new Error("Failed to 'syncToClients()', 'transports.server' does not exist."); } }; PortDetector.prototype.syncFullState = function () { return __awaiter(this, arguments, void 0, function (_a) { var fullState, store; var _b = _a === void 0 ? {} : _a, _c = _b.forceSync, forceSync = _c === void 0 ? true : _c; return __generator(this, function (_d) { switch (_d.label) { case 0: if (forceSync) { this.syncFullStatePromise = undefined; } if (!this.syncFullStatePromise) return [3 /*break*/, 2]; return [4 /*yield*/, this.syncFullStatePromise]; case 1: _d.sent(); return [2 /*return*/]; case 2: if (typeof this.transports.client === 'undefined') { throw new Error("The current client transport does not exist."); } this.syncFullStatePromise = this.transports.client.emit(loadFullStateActionName, !forceSync ? this.lastAction.sequence : -1); return [4 /*yield*/, this.syncFullStatePromise]; case 3: fullState = _d.sent(); this.syncFullStatePromise = undefined; if (typeof fullState === 'undefined') { throw new Error("Failed to sync full state from server port."); } if (fullState === null || (!forceSync && this.lastAction.sequence > fullState[this.lastAction.stateKey]._sequence)) return [2 /*return*/]; store = this[storeKey]; if (process.env.NODE_ENV !== 'production') { console.log('[syncFullState]', 'old sequence:', this.lastAction.sequence, 'new sequence:', fullState[this.lastAction.stateKey]._sequence); } store.dispatch({ type: "".concat(actionIdentifier, "_").concat(loadFullStateActionName), state: this.getNextState(fullState), _reactant: actionIdentifier, }); this.lastAction.sequence = fullState[this.lastAction.stateKey]._sequence; return [2 /*return*/]; } }); }); }; /** * ignore router state and isolated state sync for last action */ PortDetector.prototype.getNextState = function (fullState) { var store = this[storeKey]; var currentFullState = store.getState(); var nextState = __assign(__assign({}, fullState), { router: currentFullState.router }); if (this.isolatedInstanceKeys.length) { this.isolatedInstanceKeys.forEach(function (key) { if (key) { nextState[key] = currentFullState[key]; } }); } return nextState; }; /** * transform port with new transport */ PortDetector.prototype.transform = function (port, transport) { if (port !== 'server' && port !== 'client') { throw new Error("The port '".concat(port, "' is not supported.")); } this.sharedAppOptions.transports[port] = transport !== null && transport !== void 0 ? transport : this.sharedAppOptions.transports[port]; this.sharedAppOptions.transform(port); }; var _a; PortDetector = __decorate([ injectable(), __param(0, inject(SharedAppOptions)), __metadata("design:paramtypes", [Object, typeof (_a = typeof LastAction !== "undefined" && LastAction) === "function" ? _a : Object]) ], PortDetector); return PortDetector; }()); export { PortDetector };