This commit is contained in:
53
react/features/base/redux/MiddlewareRegistry.ts
Normal file
53
react/features/base/redux/MiddlewareRegistry.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { Middleware, applyMiddleware } from 'redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
|
||||
/**
|
||||
* A registry for Redux middleware, allowing features to register their
|
||||
* middleware without needing to create additional inter-feature dependencies.
|
||||
*/
|
||||
class MiddlewareRegistry {
|
||||
_elements: Array<Middleware<any, any>>;
|
||||
|
||||
/**
|
||||
* Creates a MiddlewareRegistry instance.
|
||||
*/
|
||||
constructor() {
|
||||
/**
|
||||
* The set of registered middleware.
|
||||
*
|
||||
* @private
|
||||
* @type {Middleware[]}
|
||||
*/
|
||||
this._elements = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all registered middleware into a store enhancer.
|
||||
* (@link http://redux.js.org/docs/api/applyMiddleware.html).
|
||||
*
|
||||
* @param {Middleware[]} additional - Any additional middleware that need to
|
||||
* be included (such as middleware from third-party modules).
|
||||
* @returns {Middleware}
|
||||
*/
|
||||
applyMiddleware(...additional: Array<Middleware<any, any>>) {
|
||||
return applyMiddleware(...this._elements, ...additional);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a middleware to the registry.
|
||||
*
|
||||
* The method is to be invoked only before {@link #applyMiddleware()}.
|
||||
*
|
||||
* @param {Middleware} middleware - A Redux middleware.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(middleware: Middleware<any, IReduxState, IStore['dispatch']>) {
|
||||
this._elements.push(middleware);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public singleton instance of the MiddlewareRegistry class.
|
||||
*/
|
||||
export default new MiddlewareRegistry();
|
||||
244
react/features/base/redux/PersistenceRegistry.ts
Normal file
244
react/features/base/redux/PersistenceRegistry.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import { safeJsonParse } from '@jitsi/js-utils/json';
|
||||
import md5 from 'js-md5';
|
||||
|
||||
import logger from './logger';
|
||||
|
||||
declare let __DEV__: any;
|
||||
|
||||
/**
|
||||
* Mixed type of the element (subtree) config. If it's a {@code boolean} (and is
|
||||
* {@code true}), we persist the entire subtree. If it's an {@code Object}, we
|
||||
* persist a filtered subtree based on the properties of the config object.
|
||||
*/
|
||||
declare type ElementConfig = boolean | Object;
|
||||
|
||||
/**
|
||||
* The type of the name-config pairs stored in {@code PersistenceRegistry}.
|
||||
*/
|
||||
declare type PersistencyConfigMap = { [name: string]: ElementConfig; };
|
||||
|
||||
/**
|
||||
* A registry to allow features to register their redux store subtree to be
|
||||
* persisted and also handles the persistency calls too.
|
||||
*/
|
||||
class PersistenceRegistry {
|
||||
_checksum = '';
|
||||
_defaultStates: { [name: string ]: Object | undefined; } = {};
|
||||
_elements: PersistencyConfigMap = {};
|
||||
|
||||
/**
|
||||
* Returns the persisted redux state. Takes the {@link #_elements} into
|
||||
* account as we may have persisted something in the past that we don't want
|
||||
* to retrieve anymore. The next {@link #persistState} will remove such
|
||||
* values.
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
getPersistedState() {
|
||||
const filteredPersistedState: any = {};
|
||||
|
||||
// localStorage key per feature
|
||||
for (const subtreeName of Object.keys(this._elements)) {
|
||||
// Assumes that the persisted value is stored under the same key as
|
||||
// the feature's redux state name.
|
||||
const persistedSubtree
|
||||
= this._getPersistedSubtree(
|
||||
subtreeName,
|
||||
this._elements[subtreeName],
|
||||
this._defaultStates[subtreeName]);
|
||||
|
||||
if (persistedSubtree !== undefined) {
|
||||
filteredPersistedState[subtreeName] = persistedSubtree;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the checksum.
|
||||
this._checksum = this._calculateChecksum(filteredPersistedState);
|
||||
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
logger.info('redux state rehydrated as', filteredPersistedState);
|
||||
}
|
||||
|
||||
return filteredPersistedState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a persist operation, but its execution will depend on the
|
||||
* current checksums (checks changes).
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {void}
|
||||
*/
|
||||
persistState(state: Object) {
|
||||
const filteredState = this._getFilteredState(state);
|
||||
const checksum = this._calculateChecksum(filteredState);
|
||||
|
||||
if (checksum !== this._checksum) {
|
||||
for (const subtreeName of Object.keys(filteredState)) {
|
||||
try {
|
||||
jitsiLocalStorage.setItem(subtreeName, JSON.stringify(filteredState[subtreeName]));
|
||||
} catch (error) {
|
||||
logger.error('Error persisting redux subtree', subtreeName, error);
|
||||
}
|
||||
}
|
||||
logger.info(`redux state persisted. ${this._checksum} -> ${checksum}`);
|
||||
this._checksum = checksum;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new subtree config to be used for the persistency.
|
||||
*
|
||||
* @param {string} name - The name of the subtree the config belongs to.
|
||||
* @param {ElementConfig} config - The config {@code Object}, or
|
||||
* {@code boolean} if the entire subtree needs to be persisted.
|
||||
* @param {Object} defaultState - The default state of the component. If
|
||||
* it's provided, the rehydrated state will be merged with it before it gets
|
||||
* pushed into Redux.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(
|
||||
name: string,
|
||||
config: ElementConfig = true,
|
||||
defaultState?: Object) {
|
||||
this._elements[name] = config;
|
||||
this._defaultStates[name] = defaultState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the checksum of a specific state.
|
||||
*
|
||||
* @param {Object} state - The redux state to calculate the checksum of.
|
||||
* @private
|
||||
* @returns {string} The checksum of the specified {@code state}.
|
||||
*/
|
||||
_calculateChecksum(state: Object) {
|
||||
try {
|
||||
return md5.hex(JSON.stringify(state) || '');
|
||||
} catch (error) {
|
||||
logger.error('Error calculating checksum for state', error);
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered state from the actual or the persisted redux state,
|
||||
* based on this registry.
|
||||
*
|
||||
* @param {Object} state - The actual or persisted redux state.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredState(state: any): any {
|
||||
const filteredState: any = {};
|
||||
|
||||
for (const name of Object.keys(this._elements)) {
|
||||
if (state[name]) {
|
||||
filteredState[name]
|
||||
= this._getFilteredSubtree(
|
||||
state[name],
|
||||
this._elements[name]);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a filtered subtree based on the config for persisting or for
|
||||
* retrieval.
|
||||
*
|
||||
* @param {Object} subtree - The redux state subtree.
|
||||
* @param {ElementConfig} subtreeConfig - The related config.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getFilteredSubtree(subtree: any, subtreeConfig: any) {
|
||||
let filteredSubtree: any;
|
||||
|
||||
if (typeof subtreeConfig === 'object') {
|
||||
// Only a filtered subtree gets persisted as specified by
|
||||
// subtreeConfig.
|
||||
filteredSubtree = {};
|
||||
for (const persistedKey of Object.keys(subtree)) {
|
||||
if (subtreeConfig[persistedKey]) {
|
||||
filteredSubtree[persistedKey] = subtree[persistedKey];
|
||||
}
|
||||
}
|
||||
} else if (subtreeConfig) {
|
||||
// Persist the entire subtree.
|
||||
filteredSubtree = subtree;
|
||||
}
|
||||
|
||||
return filteredSubtree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a persisted subtree from the storage.
|
||||
*
|
||||
* @param {string} subtreeName - The name of the subtree.
|
||||
* @param {Object} subtreeConfig - The config of the subtree from
|
||||
* {@link #_elements}.
|
||||
* @param {Object} subtreeDefaults - The defaults of the persisted subtree.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getPersistedSubtree(subtreeName: string, subtreeConfig: Object, subtreeDefaults?: Object) {
|
||||
let persistedSubtree = jitsiLocalStorage.getItem(subtreeName);
|
||||
|
||||
if (persistedSubtree) {
|
||||
try {
|
||||
persistedSubtree = safeJsonParse(persistedSubtree);
|
||||
|
||||
const filteredSubtree
|
||||
= this._getFilteredSubtree(persistedSubtree, subtreeConfig);
|
||||
|
||||
if (filteredSubtree !== undefined) {
|
||||
return this._mergeDefaults(
|
||||
filteredSubtree, subtreeDefaults);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error parsing persisted subtree',
|
||||
subtreeName,
|
||||
persistedSubtree,
|
||||
error);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the persisted subtree with its defaults before rehydrating the
|
||||
* values.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} subtree - The Redux subtree.
|
||||
* @param {?Object} defaults - The defaults, if any.
|
||||
* @returns {Object}
|
||||
*/
|
||||
_mergeDefaults(subtree: Object, defaults?: Object) {
|
||||
if (!defaults) {
|
||||
return subtree;
|
||||
}
|
||||
|
||||
// If the subtree is an array, we don't need to merge it with the
|
||||
// defaults, because if it has a value, it will overwrite it, and if
|
||||
// it's undefined, it won't be even returned, and Redux will natively
|
||||
// use the default values instead.
|
||||
if (!Array.isArray(subtree)) {
|
||||
return {
|
||||
...defaults,
|
||||
...subtree
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new PersistenceRegistry();
|
||||
44
react/features/base/redux/README.md
Normal file
44
react/features/base/redux/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
Jitsi Meet - redux state persistence
|
||||
====================================
|
||||
Jitsi Meet has a persistence layer that persists specific subtrees of the redux
|
||||
store/state into window.localStorage (on Web) or AsyncStorage (on mobile).
|
||||
|
||||
Usage
|
||||
=====
|
||||
If a subtree of the redux store should be persisted (e.g.
|
||||
`'features/base/settings'`), then persistence for that subtree should be
|
||||
requested by registering the subtree with `PersistenceRegistry`.
|
||||
|
||||
For example, to register the field `displayName` of the redux subtree
|
||||
`'features/base/settings'` to be persisted, use:
|
||||
```javascript
|
||||
PersistenceRegistry.register('features/base/settings', {
|
||||
displayName: true
|
||||
});
|
||||
```
|
||||
|
||||
in the `reducer.js` of the `base/settings` feature.
|
||||
|
||||
If the second parameter is omitted, the entire feature state is persisted.
|
||||
|
||||
When it's done, Jitsi Meet will automatically persist these subtrees and
|
||||
rehydrate them on startup.
|
||||
|
||||
Throttling
|
||||
==========
|
||||
To avoid too frequent write operations in the storage, we utilize throttling in
|
||||
the persistence layer, meaning that the storage gets persisted only once every 2
|
||||
seconds, even if multiple redux state changes occur during this period. The
|
||||
throttling timeout can be configured in
|
||||
```
|
||||
react/features/base/storage/middleware.js#PERSIST_STATE_DELAY
|
||||
```
|
||||
|
||||
Serialization
|
||||
=============
|
||||
The API JSON.stringify() is currently used to serialize feature states,
|
||||
therefore its limitations affect the persistency feature too. E.g. complex
|
||||
objects, such as Maps (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
|
||||
or Sets (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set)
|
||||
cannot be automatically persisted at the moment. The same applies to Functions
|
||||
(which is not a good practice to store in Redux anyhow).
|
||||
62
react/features/base/redux/ReducerRegistry.ts
Normal file
62
react/features/base/redux/ReducerRegistry.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Action, type Reducer, combineReducers } from 'redux';
|
||||
|
||||
/**
|
||||
* The type of the dictionary/map which associates a reducer (function) with the
|
||||
* name of he Redux state property managed by the reducer.
|
||||
*/
|
||||
type NameReducerMap<S> = { [name: string]: Reducer<S, Action<any>>; };
|
||||
|
||||
/**
|
||||
* A registry for Redux reducers, allowing features to register themselves
|
||||
* without needing to create additional inter-feature dependencies.
|
||||
*/
|
||||
class ReducerRegistry {
|
||||
_elements: NameReducerMap<any>;
|
||||
|
||||
/**
|
||||
* Creates a ReducerRegistry instance.
|
||||
*/
|
||||
constructor() {
|
||||
/**
|
||||
* The set of registered reducers, keyed based on the field each reducer
|
||||
* will manage.
|
||||
*
|
||||
* @private
|
||||
* @type {NameReducerMap}
|
||||
*/
|
||||
this._elements = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines all registered reducers into a single reducing function.
|
||||
*
|
||||
* @param {Object} [additional={}] - Any additional reducers that need to be
|
||||
* included (such as reducers from third-party modules).
|
||||
* @returns {Function}
|
||||
*/
|
||||
combineReducers(additional: NameReducerMap<any> = {}) {
|
||||
return combineReducers({
|
||||
...this._elements,
|
||||
...additional
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a reducer to the registry.
|
||||
*
|
||||
* The method is to be invoked only before {@link #combineReducers()}.
|
||||
*
|
||||
* @param {string} name - The field in the state object that will be managed
|
||||
* by the provided reducer.
|
||||
* @param {Reducer} reducer - A Redux reducer.
|
||||
* @returns {void}
|
||||
*/
|
||||
register<S>(name: string, reducer: Reducer<S, any>) {
|
||||
this._elements[name] = reducer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public singleton instance of the ReducerRegistry class.
|
||||
*/
|
||||
export default new ReducerRegistry();
|
||||
186
react/features/base/redux/StateListenerRegistry.ts
Normal file
186
react/features/base/redux/StateListenerRegistry.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
|
||||
import { equals } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* The type listener supported for registration with
|
||||
* {@link StateListenerRegistry} in association with a {@link Selector}.
|
||||
*
|
||||
* @param {any} selection - The value derived from the redux store/state by the
|
||||
* associated {@code Selector}. Immutable!
|
||||
* @param {Store} store - The redux store. Provided in case the {@code Listener}
|
||||
* needs to {@code dispatch} or {@code getState}. The latter is advisable only
|
||||
* if the {@code Listener} is not to respond to changes to that state.
|
||||
* @param {any} prevSelection - The value previously derived from the redux
|
||||
* store/state by the associated {@code Selector}. The {@code Listener} is
|
||||
* invoked only if {@code prevSelection} and {@code selection} are different.
|
||||
* Immutable!
|
||||
*/
|
||||
type Listener
|
||||
= (selection: any, store: IStore, prevSelection: any) => void;
|
||||
|
||||
/**
|
||||
* The type selector supported for registration with
|
||||
* {@link StateListenerRegistry} in association with a {@link Listener}.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state from which the {@code Selector} is to
|
||||
* derive data.
|
||||
* @param {any} prevSelection - The value previously derived from the redux
|
||||
* store/state by the {@code Selector}. Provided in case the {@code Selector}
|
||||
* needs to derive the returned value from the specified {@code state} and
|
||||
* {@code prevSelection}. Immutable!
|
||||
* @returns {any} The value derived from the specified {@code state} and/or
|
||||
* {@code prevSelection}. The associated {@code Listener} will only be invoked
|
||||
* if the returned value is other than {@code prevSelection}.
|
||||
*/
|
||||
type Selector = (state: IReduxState, prevSelection: any) => any;
|
||||
|
||||
/**
|
||||
* Options that can be passed to the register method.
|
||||
*/
|
||||
type RegistrationOptions = {
|
||||
|
||||
/**
|
||||
* @property {boolean} [deepEquals=false] - Whether or not a deep equals check should be performed on the selection
|
||||
* returned by {@link Selector}.
|
||||
*/
|
||||
deepEquals?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A type of a {@link Selector}-{@link Listener} association in which the
|
||||
* {@code Listener} listens to changes in the values derived from a redux
|
||||
* store/state by the {@code Selector}.
|
||||
*/
|
||||
type SelectorListener = {
|
||||
|
||||
/**
|
||||
* The {@code Listener} which listens to changes in the values selected by
|
||||
* {@link selector}.
|
||||
*/
|
||||
listener: Listener;
|
||||
|
||||
/**
|
||||
* The {@link RegistrationOptions} passed during the registration to be applied on the listener.
|
||||
*/
|
||||
options?: RegistrationOptions;
|
||||
|
||||
/**
|
||||
* The {@code Selector} which selects values whose changes are listened to
|
||||
* by {@link listener}.
|
||||
*/
|
||||
selector: Selector;
|
||||
};
|
||||
|
||||
/**
|
||||
* A registry listeners which listen to changes in a redux store/state.
|
||||
*/
|
||||
class StateListenerRegistry {
|
||||
/**
|
||||
* The {@link Listener}s registered with this {@code StateListenerRegistry}
|
||||
* to be notified when the values derived by associated {@link Selector}s
|
||||
* from a redux store/state change.
|
||||
*/
|
||||
_selectorListeners: Set<SelectorListener> = new Set();
|
||||
|
||||
/**
|
||||
* Invoked by a specific redux store any time an action is dispatched, and
|
||||
* some part of the state (tree) may potentially have changed.
|
||||
*
|
||||
* @param {Object} context - The redux store invoking the listener and the
|
||||
* private state of this {@code StateListenerRegistry} associated with the
|
||||
* redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
_listener({ prevSelections, store }: {
|
||||
prevSelections: Map<SelectorListener, any>;
|
||||
store: Store<any, any>;
|
||||
}) {
|
||||
for (const selectorListener of this._selectorListeners) {
|
||||
const prevSelection = prevSelections.get(selectorListener);
|
||||
|
||||
try {
|
||||
const selection
|
||||
= selectorListener.selector(
|
||||
store.getState(),
|
||||
prevSelection);
|
||||
const useDeepEquals = selectorListener?.options?.deepEquals;
|
||||
|
||||
if ((useDeepEquals && !equals(prevSelection, selection))
|
||||
|| (!useDeepEquals && prevSelection !== selection)) {
|
||||
prevSelections.set(selectorListener, selection);
|
||||
selectorListener.listener(selection, store, prevSelection);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't let one faulty listener prevent other listeners from
|
||||
// being notified about their associated changes.
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a specific listener to be notified when the value derived by a
|
||||
* specific {@code selector} from a redux store/state changes.
|
||||
*
|
||||
* @param {Function} selector - The pure {@code Function} of the redux
|
||||
* store/state (and the previous selection of made by {@code selector})
|
||||
* which selects the value listened to by the specified {@code listener}.
|
||||
* @param {Function} listener - The listener to register with this
|
||||
* {@code StateListenerRegistry} so that it gets invoked when the value
|
||||
* returned by the specified {@code selector} changes.
|
||||
* @param {RegistrationOptions} [options] - Any options to be applied to the registration.
|
||||
* @returns {void}
|
||||
*/
|
||||
register(selector: Selector, listener: Listener, options?: RegistrationOptions) {
|
||||
if (typeof selector !== 'function' || typeof listener !== 'function') {
|
||||
throw new Error('Invalid selector or listener!');
|
||||
}
|
||||
|
||||
this._selectorListeners.add({
|
||||
listener,
|
||||
selector,
|
||||
options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to a specific redux store (so that this instance gets notified
|
||||
* any time an action is dispatched, and some part of the state (tree) of
|
||||
* the specified redux store may potentially have changed).
|
||||
*
|
||||
* @param {Store} store - The redux store to which this
|
||||
* {@code StateListenerRegistry} is to {@code subscribe}.
|
||||
* @returns {void}
|
||||
*/
|
||||
subscribe(store: Store<any, any>) {
|
||||
// XXX If StateListenerRegistry is not utilized by the app to listen to
|
||||
// state changes, do not bother subscribing to the store at all.
|
||||
if (this._selectorListeners.size) {
|
||||
store.subscribe(
|
||||
this._listener.bind(
|
||||
this,
|
||||
{
|
||||
/**
|
||||
* The previous selections of the {@code Selector}s
|
||||
* registered with this {@code StateListenerRegistry}.
|
||||
*
|
||||
* @type Map<any>
|
||||
*/
|
||||
prevSelections: new Map(),
|
||||
|
||||
/**
|
||||
* The redux store.
|
||||
*
|
||||
* @type Store
|
||||
*/
|
||||
store
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new StateListenerRegistry();
|
||||
145
react/features/base/redux/functions.ts
Normal file
145
react/features/base/redux/functions.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { IStateful } from '../app/types';
|
||||
|
||||
/**
|
||||
* Sets specific properties of a specific state to specific values and prevents
|
||||
* unnecessary state changes.
|
||||
*
|
||||
* @param {T} target - The state on which the specified properties are to
|
||||
* be set.
|
||||
* @param {T} source - The map of properties to values which are to be set
|
||||
* on the specified target.
|
||||
* @returns {T} The specified target if the values of the specified
|
||||
* properties equal the specified values; otherwise, a new state constructed
|
||||
* from the specified target by setting the specified properties to the
|
||||
* specified values.
|
||||
*/
|
||||
export function assign<T extends Object>(target: T, source: Partial<T>): T {
|
||||
let t = target;
|
||||
|
||||
for (const property in source) { // eslint-disable-line guard-for-in
|
||||
t = _set(t, property, source[property], t === target);
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether {@code a} equals {@code b} according to deep comparison
|
||||
* (which makes sense for Redux and its state definition).
|
||||
*
|
||||
* @param {*} a - The value to compare to {@code b}.
|
||||
* @param {*} b - The value to compare to {@code a}.
|
||||
* @returns {boolean} True if {@code a} equals {@code b} (according to deep
|
||||
* comparison); false, otherwise.
|
||||
*/
|
||||
export function equals(a: any, b: any) {
|
||||
return isEqual(a, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific property of a specific state to a specific value. Prevents
|
||||
* unnecessary state changes (when the specified {@code value} is equal to the
|
||||
* value of the specified {@code property} of the specified {@code state}).
|
||||
*
|
||||
* @param {T} state - The (Redux) state from which a new state is to be
|
||||
* constructed by setting the specified {@code property} to the specified
|
||||
* {@code value}.
|
||||
* @param {string} property - The property of {@code state} which is to be
|
||||
* assigned the specified {@code value} (in the new state).
|
||||
* @param {*} value - The value to assign to the specified {@code property}.
|
||||
* @returns {T} The specified {@code state} if the value of the specified
|
||||
* {@code property} equals the specified <tt>value/tt>; otherwise, a new state
|
||||
* constructed from the specified {@code state} by setting the specified
|
||||
* {@code property} to the specified {@code value}.
|
||||
*/
|
||||
export function set<T extends Object>(state: T, property: keyof T, value: any): T {
|
||||
return _set(state, property, value, /* copyOnWrite */ true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specific property of a specific state to a specific value. Prevents
|
||||
* unnecessary state changes (when the specified {@code value} is equal to the
|
||||
* value of the specified {@code property} of the specified {@code state}).
|
||||
*
|
||||
* @param {T} state - The (Redux) state from which a state is to be
|
||||
* constructed by setting the specified {@code property} to the specified
|
||||
* {@code value}.
|
||||
* @param {string} property - The property of {@code state} which is to be
|
||||
* assigned the specified {@code value}.
|
||||
* @param {*} value - The value to assign to the specified {@code property}.
|
||||
* @param {boolean} copyOnWrite - If the specified {@code state} is to not be
|
||||
* modified, {@code true}; otherwise, {@code false}.
|
||||
* @returns {T} The specified {@code state} if the value of the specified
|
||||
* {@code property} equals the specified <tt>value/tt> or {@code copyOnWrite}
|
||||
* is truthy; otherwise, a new state constructed from the specified
|
||||
* {@code state} by setting the specified {@code property} to the specified
|
||||
* {@code value}.
|
||||
*/
|
||||
function _set<T extends Object>(
|
||||
state: T,
|
||||
property: keyof T,
|
||||
value: any,
|
||||
copyOnWrite: boolean): T {
|
||||
// Delete state properties that are to be set to undefined. (It is a matter
|
||||
// of personal preference, mostly.)
|
||||
if (typeof value === 'undefined'
|
||||
&& Object.prototype.hasOwnProperty.call(state, property)) {
|
||||
const newState = copyOnWrite ? { ...state } : state;
|
||||
|
||||
if (delete newState[property]) {
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
|
||||
if (state[property] !== value) {
|
||||
if (copyOnWrite) {
|
||||
return {
|
||||
...state,
|
||||
[property]: value
|
||||
};
|
||||
}
|
||||
|
||||
state[property] = value;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
/**
|
||||
* Whether or not the entity is of type IStore.
|
||||
*
|
||||
* @param {IStateful} stateful - The entity to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isStore(stateful: IStateful): stateful is IStore {
|
||||
return 'getState' in stateful && typeof stateful.getState === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns redux state from the specified {@code stateful} which is presumed to
|
||||
* be related to the redux state (e.g. The redux store, the redux
|
||||
* {@code getState} function).
|
||||
*
|
||||
* @param {Function|IStore} stateful - The entity such as the redux store or the
|
||||
* redux {@code getState} function from which the redux state is to be
|
||||
* returned.
|
||||
* @returns {Object} The redux state.
|
||||
*/
|
||||
export function toState(stateful: IStateful): IReduxState {
|
||||
if (stateful) {
|
||||
if (typeof stateful === 'function') {
|
||||
return stateful();
|
||||
}
|
||||
|
||||
if (isStore(stateful)) {
|
||||
return stateful.getState();
|
||||
}
|
||||
}
|
||||
|
||||
return stateful;
|
||||
}
|
||||
3
react/features/base/redux/logger.ts
Normal file
3
react/features/base/redux/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/redux');
|
||||
43
react/features/base/redux/middleware.ts
Normal file
43
react/features/base/redux/middleware.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import MiddlewareRegistry from './MiddlewareRegistry';
|
||||
import PersistenceRegistry from './PersistenceRegistry';
|
||||
import { toState } from './functions';
|
||||
|
||||
/**
|
||||
* The delay in milliseconds that passes between the last state change and the
|
||||
* persisting of that state in the storage.
|
||||
*/
|
||||
const PERSIST_STATE_DELAY = 2000;
|
||||
|
||||
/**
|
||||
* A throttled function to avoid repetitive state persisting.
|
||||
*/
|
||||
const throttledPersistState = throttle(state => PersistenceRegistry.persistState(state), PERSIST_STATE_DELAY);
|
||||
|
||||
// Web only code.
|
||||
// We need the <tt>if</tt> because it appears that on mobile the polyfill is not
|
||||
// executed yet.
|
||||
if (typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('unload', () => {
|
||||
throttledPersistState.flush();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A master MiddleWare to selectively persist state. Please use the
|
||||
* {@link persisterconfig.json} to set which subtrees of the redux state should
|
||||
* be persisted.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const oldState = toState(store);
|
||||
const result = next(action);
|
||||
const newState = toState(store);
|
||||
|
||||
oldState === newState || throttledPersistState(newState);
|
||||
|
||||
return result;
|
||||
});
|
||||
Reference in New Issue
Block a user