reactant-share
Version:
A framework for building shared web applications with Reactant
645 lines (642 loc) • 30.3 kB
JavaScript
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 };