UNPKG

reactant-share

Version:

A framework for building shared web applications with Reactant

645 lines (642 loc) 30.3 kB
import { __extends, __awaiter, __decorate, __metadata, __param, __assign, __generator, __spreadArray, __read } from '../node_modules/tslib/tslib.es6.js'; import { watch, stateKey, modulesKey, state, action, injectable, inject } from 'reactant'; import { LOCATION_CHANGE, RouterOptions, Router } from 'reactant-router'; export { RouterOptions, createBrowserHistory, createHashHistory, createMemoryHistory } from 'reactant-router'; import { storageModuleName, routerModuleName, SharedAppOptions, syncWorkerRouterName, syncRouterName } from '../constants.js'; import { PortDetector } from './portDetector.js'; import { delegate } from '../delegate.js'; import { fork } from '../fork.js'; import { isSharedWorker } from '../utils.js'; var ReactantRouter = /** @class */ (function (_super) { __extends(ReactantRouter, _super); function ReactantRouter(portDetector, sharedAppOptions, options) { var _a; var _this = _super.call(this, __assign(__assign({}, options), { autoCreateHistory: !((sharedAppOptions.type === 'SharedWorker' && sharedAppOptions.port === 'server') || !globalThis.document) })) || this; _this.portDetector = portDetector; _this.sharedAppOptions = sharedAppOptions; _this.options = options; _this.passiveRoute = false; _this.cachedHistory = []; _this.forwardHistory = []; /** * The timestamp of the last routing. */ _this.lastRoutedTimestamp = isSharedWorker ? 0 : Date.now(); _this.toBeRouted = null; _this._routers = (_a = {}, _a[_this.portDetector.name] = _this.router, _a); _this.defaultHistory = { action: 'POP', location: { pathname: _this.defaultRoute, search: '', hash: '', state: undefined, }, }; _this.firstRenderingSync = new Promise(function (resolve) { _this.firstRenderingSyncResolve = resolve; }); _this.firstActiveSync = new Promise(function (resolve) { _this.firstActiveSyncResolve = resolve; }); _this.firstClientSync = Promise.all([ _this.firstRenderingSync, _this.firstActiveSync, ]); _this.portDetector.onClient(function () { var stopWatching = watch(_this, function () { return _this.portDetector.lastAction.action; }, function () { var action = _this.portDetector.lastAction .action; if (action.type === LOCATION_CHANGE && action.payload.isFirstRendering) { stopWatching(); var router = _this._routers[_this.portDetector.name]; if (router && _this.compareRouter(router, _this.router)) { // router reducer @@router/LOCATION_CHANGE event and syncFullState event The events may be out of order, so we re-check route consistency after synchronizing the state. _this.history.replace(router.location); } _this.firstRenderingSyncResolve(); } }); }); if (globalThis.document) { window.addEventListener('popstate', function () { if (!_this.passiveRoute) { _this.lastRoutedTimestamp = Date.now(); } }); } if (!_this.portDetector.shared) { var stopWatching_1 = _this.watchRehydratedRouting(); watch(_this, function () { return _this.router; }, function () { if (_this.router) { // just update the current router to routers mapping by name _this._setRouters(_this.portDetector.name, _this.router); } if (!_this.enableCacheRouting) { stopWatching_1(); } }); } _this.portDetector.onClient(function () { if (!_this.portDetector.sharedAppOptions.forcedSyncClient) { var visibilitychange_1 = function () { return __awaiter(_this, void 0, void 0, function () { var fn; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!(document.visibilityState === 'visible')) return [3 /*break*/, 2]; return [4 /*yield*/, this.portDetector.syncFullState({ forceSync: false })]; case 1: _a.sent(); if (this.toBeRouted) { fn = this.toBeRouted; this.toBeRouted = null; fn(); } _a.label = 2; case 2: return [2 /*return*/]; } }); }); }; document.addEventListener('visibilitychange', visibilitychange_1); return function () { document.removeEventListener('visibilitychange', visibilitychange_1); }; } }); // #region sync init router from clients in Worker mode _this.portDetector.onServer(function (transport) { watch(_this, function () { return _this.router; }, function (router) { if (router && (!_this.cachedHistory[0] || _this.compareRouter(router, _this.cachedHistory[0]))) { if (router.action === 'REPLACE') { _this.cachedHistory[0] = router; } else { _this.cachedHistory.unshift(router); } _this.cachedHistory.length = _this.maxHistoryLength; // Limit the length of the historical stack } }); if (_this.portDetector.isWorkerMode && !_this.enableCacheRouting) { transport .emit(syncWorkerRouterName, _this.portDetector.name) .then(function (router) { if (router) { _this._changeRoutingOnSever(_this.portDetector.name, router, Date.now()); } }); } else if (_this.enableCacheRouting) { return _this.watchRehydratedRouting(); } }); _this.portDetector.onClient(function (transport) { if (_this.portDetector.isWorkerMode) { return transport.listen(syncWorkerRouterName, function (name) { return __awaiter(_this, void 0, void 0, function () { return __generator(this, function (_a) { if (name === this.portDetector.name) { return [2 /*return*/, this.router]; } return [2 /*return*/]; }); }); }); } }); // #endregion // #region watch router and sync up router to all clients and server port _this.portDetector.onClient(function () { return watch(_this, function () { return _this.router; }, function () { delegate(_this, '_changeRoutingOnSever', [ _this.portDetector.name, _this.router, _this.lastRoutedTimestamp, _this.portDetector.clientId, ], { respond: false, }); }); }); _this.portDetector.onServer(function () { return watch(_this, function () { return _this.router; }, function () { if (!_this.portDetector.isWorkerMode) { if (globalThis.document) { // just update the current router to routers mapping by name at every time in shared tab mode _this._setRouters(_this.portDetector.name, _this.router); } fork(_this, '_changeRoutingOnClient', [_this.portDetector.name, _this.router, _this.lastRoutedTimestamp], { silent: true, }); } }); }); // #endregion // #region sync init router from server port in all modes _this.portDetector.onServer(function (transport) { var rehydratedPromise = _this.enableCacheRouting ? new Promise(function (resolve) { var stopWatching = watch(_this, function () { var _a; return (_a = _this[stateKey]._persist) === null || _a === void 0 ? void 0 : _a.rehydrated; }, function (rehydrated) { if (rehydrated) { stopWatching(); resolve(); } }); }) : Promise.resolve(); return transport.listen(syncRouterName, function (name, timestamp, router) { return __awaiter(_this, void 0, void 0, function () { var currentRouter; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, rehydratedPromise]; case 1: _a.sent(); currentRouter = this._routers[name]; if (!currentRouter && router) { this._changeRoutingOnSever(name, router, timestamp); } return [2 /*return*/, currentRouter]; } }); }); }); }); _this.portDetector.onClient(function (transport) { transport .emit(syncRouterName, _this.portDetector.name, _this.lastRoutedTimestamp, _this.router) .then(function (router) { if (!router) { _this.firstActiveSyncResolve(); return; } _this.passiveRoute = true; _this.history.replace(router.location); _this.passiveRoute = false; _this.firstActiveSyncResolve(); }); }); return _this; // #endregion } Object.defineProperty(ReactantRouter.prototype, "maxHistoryLength", { get: function () { var _a; return (_a = this.options.maxHistoryLength) !== null && _a !== void 0 ? _a : 50; }, enumerable: false, configurable: true }); ReactantRouter.prototype.watchRehydratedRouting = function () { var _this = this; // The first rendering and the hydration of the persistent router may emit actions in a different order due to the module order. var firstTrigger = false; var stopWatchingRehydrated = watch(this, function () { var _a; return (_a = _this[stateKey]._persist) === null || _a === void 0 ? void 0 : _a.rehydrated; }, function (rehydrated) { if (!_this.enableCacheRouting) { stopWatchingRehydrated(); } if (rehydrated) { stopWatchingRehydrated(); if (!firstTrigger) { firstTrigger = true; return; } var router = _this._routers[_this.portDetector.name]; _this._changeRoutingOnSever(_this.portDetector.name, router !== null && router !== void 0 ? router : _this.defaultHistory, Date.now()); } }); var stopWatchingIsFirstRendering = watch(this, function () { return _this.portDetector.lastAction.action; }, function () { if (!_this.enableCacheRouting) { stopWatchingIsFirstRendering(); } var action = _this.portDetector.lastAction .action; if (action.type === LOCATION_CHANGE && action.payload.isFirstRendering) { stopWatchingIsFirstRendering(); if (!firstTrigger) { firstTrigger = true; return; } var router = _this._routers[_this.portDetector.name]; _this._changeRoutingOnSever(_this.portDetector.name, router !== null && router !== void 0 ? router : _this.defaultHistory, Date.now()); } }); return function () { stopWatchingRehydrated(); stopWatchingIsFirstRendering(); }; }; ReactantRouter.prototype.compareRouter = function (router1, router2) { return (router1.location.pathname !== router2.location.pathname || router1.location.hash !== router2.location.hash || router1.location.search !== router2.location.search || JSON.stringify(router1.location.state) !== JSON.stringify(router2.location.state) || JSON.stringify(router1.location.query) !== JSON.stringify(router2.location.query)); }; ReactantRouter.prototype._changeRoutingOnSever = function (name, router, timestamp, clientId) { if (!this.portDetector.isServerWorker && globalThis.document) { // Only update the latest routes if (this.lastRoutedTimestamp >= timestamp) return; this.lastRoutedTimestamp = timestamp; } this._setRouters(name, router); if (name === this.portDetector.name) { if (this.portDetector.isWorkerMode) { this.dispatchChanged(router); } else if (this.compareRouter(router, this.router)) { this.passiveRoute = true; this.history.push(router.location); this.passiveRoute = false; } if (this.portDetector.shared) { fork(this, '_changeRoutingOnClient', [this.portDetector.name, this.router, timestamp], { silent: true, clientIds: clientId ? // Skip routing the origin of the client this.portDetector.clientIds.filter(function (id) { return id !== clientId; }) : undefined, }); } } else if (this.portDetector.shared) { fork(this, '_changeRoutingOnClient', [name, router, timestamp], { silent: true, clientIds: clientId ? // Skip routing the origin of the client this.portDetector.clientIds.filter(function (id) { return id !== clientId; }) : undefined, }); } }; ReactantRouter.prototype._changeRoutingOnClient = function (name, router, timestamp) { var _this = this; // if the client is the non-origin of the routing, skip it // or if the timestamp of the routing is earlier than the last routing, skip it if (name !== this.portDetector.name || (timestamp && this.lastRoutedTimestamp >= timestamp)) return; var route = function () { if (_this.history && _this.compareRouter(router, _this.router)) { _this.passiveRoute = true; _this.history.push(router.location); _this.passiveRoute = false; } }; if (this.portDetector.disableSyncClient) { this.toBeRouted = route; } else { route(); } }; ReactantRouter.prototype._makeRoutingOnClient = function (_a) { var _this = this; var args = _a.args, action = _a.action, name = _a.name; return new Promise(function (resolve) { var route = function () { if (name === _this.portDetector.name) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore _super.prototype[action].apply(_this, __spreadArray([], __read(args), false)); resolve(_this.router); } }; if (_this.portDetector.disableSyncClient) { _this.toBeRouted = route; } else { route(); } }); }; ReactantRouter.prototype._setRouters = function (name, router) { var _a; if (!this.enableCacheRouting || (this.enableCacheRouting && ((_a = this[stateKey]._persist) === null || _a === void 0 ? void 0 : _a.rehydrated))) { this._routers[name] = router; } }; Object.defineProperty(ReactantRouter.prototype, "defaultRoute", { // The server port routing state is received asynchronously, so there should be a default route. get: function () { var _a; return (_a = this.options.defaultRoute) !== null && _a !== void 0 ? _a : '/'; }, enumerable: false, configurable: true }); Object.defineProperty(ReactantRouter.prototype, "enableCacheRouting", { get: function () { var _a, _b; var Storage = this[modulesKey][storageModuleName]; var routerPersistConfig = Storage === null || Storage === void 0 ? void 0 : Storage.persistConfig[routerModuleName]; return (routerPersistConfig && (((_a = routerPersistConfig.whitelist) === null || _a === void 0 ? void 0 : _a.includes('_routers')) || ((_b = routerPersistConfig.blacklist) === null || _b === void 0 ? void 0 : _b.includes('_routers')) === false)); }, enumerable: false, configurable: true }); ReactantRouter.prototype.dispatchChanged = function (router) { var _a; if (!router) return; (_a = this.store) === null || _a === void 0 ? void 0 : _a.dispatch(this.onLocationChanged(router.location, router.action)); }; Object.defineProperty(ReactantRouter.prototype, "currentPath", { get: function () { var _a, _b; return (_b = (_a = this.router) === null || _a === void 0 ? void 0 : _a.location.pathname) !== null && _b !== void 0 ? _b : this.defaultRoute; }, enumerable: false, configurable: true }); ReactantRouter.prototype.push = function (path, locationState) { return __awaiter(this, void 0, void 0, function () { var router; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.portDetector.isServerWorker) return [3 /*break*/, 2]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [path, locationState], action: 'push', name: this.portDetector.name, }, ])]; case 1: router = _a.sent(); this.dispatchChanged(router); return [3 /*break*/, 3]; case 2: this.lastRoutedTimestamp = Date.now(); _super.prototype.push.call(this, path, locationState); _a.label = 3; case 3: return [2 /*return*/]; } }); }); }; ReactantRouter.prototype.replace = function (path, locationState) { return __awaiter(this, void 0, void 0, function () { var router; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.portDetector.isServerWorker) return [3 /*break*/, 2]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [path, locationState], action: 'replace', name: this.portDetector.name, }, ])]; case 1: router = _a.sent(); this.dispatchChanged(router); return [3 /*break*/, 3]; case 2: this.lastRoutedTimestamp = Date.now(); _super.prototype.replace.call(this, path, locationState); _a.label = 3; case 3: return [2 /*return*/]; } }); }); }; ReactantRouter.prototype.go = function (n) { return __awaiter(this, void 0, void 0, function () { var stepsBack, currentRouter, targetRouter, router, stepsForward, targetRouter, router; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.portDetector.shared) { this.lastRoutedTimestamp = Date.now(); _super.prototype.go.call(this, n); return [2 /*return*/]; } if (this.portDetector.isClient) { return [2 /*return*/, delegate(this, 'go', [n])]; } if (!(n < 0)) return [3 /*break*/, 6]; stepsBack = Math.abs(n); if (!(this.cachedHistory.length > stepsBack)) return [3 /*break*/, 4]; currentRouter = this.cachedHistory.shift(); this.forwardHistory.unshift(currentRouter); // Add to forward history targetRouter = this.cachedHistory[stepsBack - 1]; if (!targetRouter) return [3 /*break*/, 2]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [ targetRouter.location.pathname, targetRouter.location.state, ], action: 'replace', name: this.portDetector.name, }, ])]; case 1: router = _a.sent(); this.dispatchChanged(router); return [3 /*break*/, 3]; case 2: console.warn('No more history to go back.'); _a.label = 3; case 3: return [3 /*break*/, 5]; case 4: console.warn('No more history to go back.'); _a.label = 5; case 5: return [3 /*break*/, 13]; case 6: if (!(n > 0)) return [3 /*break*/, 12]; stepsForward = n; if (!(this.forwardHistory.length >= stepsForward)) return [3 /*break*/, 10]; targetRouter = this.forwardHistory[stepsForward - 1]; if (!targetRouter) return [3 /*break*/, 8]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [ targetRouter.location.pathname, targetRouter.location.state, ], action: 'push', name: this.portDetector.name, }, ])]; case 7: router = _a.sent(); this.dispatchChanged(router); return [3 /*break*/, 9]; case 8: console.warn('No more history to go forward.'); _a.label = 9; case 9: // Remove the used entry from the forward stack this.forwardHistory.splice(0, stepsForward); return [3 /*break*/, 11]; case 10: console.warn('No more history to go forward.'); _a.label = 11; case 11: return [3 /*break*/, 13]; case 12: // Go to the current route (refresh the page) console.warn('Going to the current route (n = 0) does nothing.'); _a.label = 13; case 13: return [2 /*return*/]; } }); }); }; ReactantRouter.prototype.goBack = function () { return __awaiter(this, void 0, void 0, function () { var currentRouter, previousRouter, router; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.portDetector.shared) { this.lastRoutedTimestamp = Date.now(); _super.prototype.goBack.call(this); return [2 /*return*/]; } if (this.portDetector.isClient) { return [2 /*return*/, delegate(this, 'goBack', [])]; } if (!(this.cachedHistory.length > 1)) return [3 /*break*/, 4]; currentRouter = this.cachedHistory.shift(); this.forwardHistory.unshift(currentRouter); // Push to forward stack this.forwardHistory.length = this.maxHistoryLength; // Limit the length of the forward stack previousRouter = this.cachedHistory[0]; if (!previousRouter) return [3 /*break*/, 2]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [ previousRouter.location.pathname, previousRouter.location.state, ], action: 'push', name: this.portDetector.name, }, ])]; case 1: router = _a.sent(); this.dispatchChanged(router); return [3 /*break*/, 3]; case 2: console.warn('No forward route available.'); _a.label = 3; case 3: return [3 /*break*/, 5]; case 4: console.warn('No previous route available.'); _a.label = 5; case 5: return [2 /*return*/]; } }); }); }; ReactantRouter.prototype.goForward = function () { return __awaiter(this, void 0, void 0, function () { var nextRouter, router; return __generator(this, function (_a) { switch (_a.label) { case 0: if (!this.portDetector.shared) { this.lastRoutedTimestamp = Date.now(); _super.prototype.goForward.call(this); return [2 /*return*/]; } if (this.portDetector.isClient) { return [2 /*return*/, delegate(this, 'goForward', [])]; } if (!(this.forwardHistory.length > 0)) return [3 /*break*/, 3]; nextRouter = this.forwardHistory.shift(); if (!nextRouter) return [3 /*break*/, 2]; return [4 /*yield*/, fork(this, '_makeRoutingOnClient', [ { args: [nextRouter.location.pathname, nextRouter.location.state], action: 'push', name: this.portDetector.name, }, ])]; case 1: router = _a.sent(); this.dispatchChanged(router); _a.label = 2; case 2: return [3 /*break*/, 4]; case 3: console.warn('No forward route available.'); _a.label = 4; case 4: return [2 /*return*/]; } }); }); }; __decorate([ state, __metadata("design:type", Object) ], ReactantRouter.prototype, "_routers", void 0); __decorate([ action, __metadata("design:type", Function), __metadata("design:paramtypes", [String, Object]), __metadata("design:returntype", void 0) ], ReactantRouter.prototype, "_setRouters", null); ReactantRouter = __decorate([ injectable({ name: routerModuleName, }), __param(1, inject(SharedAppOptions)), __param(2, inject(RouterOptions)), __metadata("design:paramtypes", [PortDetector, Object, Object]) ], ReactantRouter); return ReactantRouter; }(Router)); export { ReactantRouter as Router };