init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View 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();

View 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();

View 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).

View 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();

View 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();

View 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;
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../logging/functions';
export default getLogger('features/base/redux');

View 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;
});