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