From 62c976279318ab96da678984e4f0814ba61d0d85 Mon Sep 17 00:00:00 2001 From: Lyubo Marinov Date: Mon, 5 Feb 2018 15:26:01 -0600 Subject: [PATCH] [RN] Protect AbstractApp and localStorage initialization --- react/features/app/components/AbstractApp.js | 46 +++++--- react/features/base/storage/native/Storage.js | 109 +++++++++++------- 2 files changed, 94 insertions(+), 61 deletions(-) diff --git a/react/features/app/components/AbstractApp.js b/react/features/app/components/AbstractApp.js index d2c741606..f9a1c22b1 100644 --- a/react/features/app/components/AbstractApp.js +++ b/react/features/app/components/AbstractApp.js @@ -97,17 +97,22 @@ export class AbstractApp extends Component { }; /** - * This way we make the mobile version wait until the - * {@code AsyncStorage} implementation of {@code Storage} - * properly initializes. On web it does actually nothing, see - * {@link #_initStorage}. + * Make the mobile {@code AbstractApp} wait until the + * {@code AsyncStorage} implementation of {@code Storage} initializes + * fully. + * + * @private + * @see {@link #_initStorage} + * @type {Promise} */ - this.init = this._initStorage().then(() => { - this.setState({ - route: undefined, - store: this._maybeCreateStore(props) - }); - }); + this._init + = this._initStorage() + .catch(() => { /* AbstractApp should always initialize! */ }) + .then(() => + this.setState({ + route: undefined, + store: this._maybeCreateStore(props) + })); } /** @@ -117,7 +122,7 @@ export class AbstractApp extends Component { * @inheritdoc */ componentWillMount() { - this.init.then(() => { + this._init.then(() => { const { dispatch } = this._getStore(); dispatch(appWillMount(this)); @@ -175,7 +180,7 @@ export class AbstractApp extends Component { componentWillReceiveProps(nextProps) { const { props } = this; - this.init.then(() => { + this._init.then(() => { // The consumer of this AbstractApp did not provide a redux store. if (typeof nextProps.store === 'undefined' @@ -236,18 +241,21 @@ export class AbstractApp extends Component { } /** - * Delays app start until the {@code Storage} implementation initialises. - * This is instantaneous on web, but is async on mobile. + * Delays this {@code AbstractApp}'s startup until the {@code Storage} + * implementation of {@code localStorage} initializes. While the + * initialization is instantaneous on Web (with Web Storage API), it is + * asynchronous on mobile/react-native. * * @private - * @returns {ReactElement} + * @returns {Promise} */ _initStorage() { - if (typeof window.localStorage._initialized !== 'undefined') { - return window.localStorage._initialized; - } + const localStorageInitializing = window.localStorage._initializing; - return Promise.resolve(); + return ( + typeof localStorageInitializing === 'undefined' + ? Promise.resolve() + : localStorageInitializing); } /** diff --git a/react/features/base/storage/native/Storage.js b/react/features/base/storage/native/Storage.js index 396ecccd5..3f35e3426 100644 --- a/react/features/base/storage/native/Storage.js +++ b/react/features/base/storage/native/Storage.js @@ -30,14 +30,77 @@ export default class Storage { */ this._keyPrefix = keyPrefix; + // Perform optional asynchronous initialization. + const initializing = this._initializeAsync(); + + if (initializing) { + // Indicate that asynchronous initialization is under way. + this._initializing = initializing; + + // When the asynchronous initialization completes, indicate its + // completion. + initializing.finally(() => { + if (this._initializing === initializing) { + this._initializing = undefined; + } + }); + } + } + + /** + * Removes all keys from this storage. + * + * @returns {void} + */ + clear() { + for (const key of Object.keys(this)) { + this.removeItem(key); + } + } + + /** + * Returns the value associated with a specific key in this storage. + * + * @param {string} key - The name of the key to retrieve the value of. + * @returns {string|null} The value associated with {@code key} or + * {@code null}. + */ + getItem(key) { + return this.hasOwnProperty(key) ? this[key] : null; + } + + /** + * Returns the value associated with a specific key in this {@code Storage} + * in an async manner. The method is required for the cases where we need + * the stored data but we're not sure yet whether this {@code Storage} is + * already initialized (e.g. on app start). + * + * @param {string} key - The name of the key to retrieve the value of. + * @returns {Promise} + */ + _getItemAsync(key) { + return ( + (this._initializing || Promise.resolve()) + .catch(() => { /* _getItemAsync should always resolve! */ }) + .then(() => this.getItem(key))); + } + + /** + * Performs asynchronous initialization of this {@code Storage} instance + * such as loading all keys from {@link AsyncStorage}. + * + * @private + * @returns {Promise} + */ + _initializeAsync() { if (typeof this._keyPrefix !== 'undefined') { // Load all previously persisted data items from React Native's // AsyncStorage. - this._initialized = new Promise(resolve => { + return new Promise(resolve => { AsyncStorage.getAllKeys().then((...getAllKeysCallbackArgs) => { - // XXX The keys argument of getAllKeys' callback may - // or may not be preceded by an error argument. + // XXX The keys argument of getAllKeys' callback may or may + // not be preceded by an error argument. const keys = getAllKeysCallbackArgs[ getAllKeysCallbackArgs.length - 1 @@ -73,46 +136,8 @@ export default class Storage { }); }); } - } - /** - * Removes all keys from this storage. - * - * @returns {void} - */ - clear() { - for (const key of Object.keys(this)) { - this.removeItem(key); - } - } - - /** - * Returns the value associated with a specific key in this storage. - * - * @param {string} key - The name of the key to retrieve the value of. - * @returns {string|null} The value associated with {@code key} or - * {@code null}. - */ - getItem(key) { - return this.hasOwnProperty(key) ? this[key] : null; - } - - /** - * Returns the value associated with a specific key in this storage in an - * async manner. This method is required for those cases where we need the - * stored data but we're not sure yet whether the {@code Storage} is already - * initialised or not - e.g. on app start. - * - * @param {string} key - The name of the key to retrieve the value of. - * @private - * @returns {Promise} - */ - _getItemAsync(key) { - return new Promise( - resolve => - AsyncStorage.getItem( - `${String(this._keyPrefix)}${key}`, - (error, result) => resolve(result ? result : null))); + return undefined; } /**