[RN] Protect AbstractApp and localStorage initialization

This commit is contained in:
Lyubo Marinov 2018-02-05 15:26:01 -06:00
parent d7dddb2509
commit 62c9762793
2 changed files with 94 additions and 61 deletions

View File

@ -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);
}
/**

View File

@ -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;
}
/**