193 lines
6.6 KiB
JavaScript
193 lines
6.6 KiB
JavaScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
/**
|
|
* A Web Sorage API implementation used for polyfilling
|
|
* {@code window.localStorage} and/or {@code window.sessionStorage}.
|
|
* <p>
|
|
* The Web Storage API is synchronous whereas React Native's builtin generic
|
|
* storage API {@code AsyncStorage} is asynchronous so the implementation with
|
|
* persistence is optimistic: it will first store the value locally in memory so
|
|
* that results can be served synchronously and then persist the value
|
|
* asynchronously. If an asynchronous operation produces an error, it's ignored.
|
|
*/
|
|
export default class Storage {
|
|
/**
|
|
* Initializes a new {@code Storage} instance. Loads all previously
|
|
* persisted data items from React Native's {@code AsyncStorage} if
|
|
* necessary.
|
|
*
|
|
* @param {string|undefined} keyPrefix - The prefix of the
|
|
* {@code AsyncStorage} keys to be persisted by this storage.
|
|
*/
|
|
constructor(keyPrefix) {
|
|
/**
|
|
* The prefix of the {@code AsyncStorage} keys persisted by this
|
|
* storage. If {@code undefined}, then the data items stored in this
|
|
* storage will not be persisted.
|
|
*
|
|
* @private
|
|
* @type {string}
|
|
*/
|
|
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.
|
|
|
|
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.
|
|
const keys
|
|
= getAllKeysCallbackArgs[
|
|
getAllKeysCallbackArgs.length - 1
|
|
].filter(key => key.startsWith(this._keyPrefix));
|
|
|
|
AsyncStorage.multiGet(keys)
|
|
.then((...multiGetCallbackArgs) => {
|
|
// XXX The result argument of multiGet may or may not be
|
|
// preceded by an errors argument.
|
|
const result
|
|
= multiGetCallbackArgs[
|
|
multiGetCallbackArgs.length - 1
|
|
];
|
|
const keyPrefixLength
|
|
= this._keyPrefix && this._keyPrefix.length;
|
|
|
|
// eslint-disable-next-line prefer-const
|
|
for (let [ key, value ] of result) {
|
|
key = key.substring(keyPrefixLength);
|
|
|
|
// XXX The loading of the previously persisted data
|
|
// items from AsyncStorage is asynchronous which
|
|
// means that it is technically possible to invoke
|
|
// setItem with a key before the key is loaded from
|
|
// AsyncStorage.
|
|
if (!this.hasOwnProperty(key)) {
|
|
this[key] = value;
|
|
}
|
|
}
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Returns the name of the nth key in this storage.
|
|
*
|
|
* @param {number} n - The zero-based integer index of the key to get the
|
|
* name of.
|
|
* @returns {string} The name of the nth key in this storage.
|
|
*/
|
|
key(n) {
|
|
const keys = Object.keys(this);
|
|
|
|
return n < keys.length ? keys[n] : null;
|
|
}
|
|
|
|
/**
|
|
* Returns an integer representing the number of data items stored in this
|
|
* storage.
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
get length() {
|
|
return Object.keys(this).length;
|
|
}
|
|
|
|
/**
|
|
* Removes a specific key from this storage.
|
|
*
|
|
* @param {string} key - The name of the key to remove.
|
|
* @returns {void}
|
|
*/
|
|
removeItem(key) {
|
|
delete this[key];
|
|
typeof this._keyPrefix === 'undefined'
|
|
|| AsyncStorage.removeItem(`${String(this._keyPrefix)}${key}`);
|
|
}
|
|
|
|
/**
|
|
* Adds a specific key to this storage and associates it with a specific
|
|
* value. If the key exists already, updates its value.
|
|
*
|
|
* @param {string} key - The name of the key to add/update.
|
|
* @param {string} value - The value to associate with {@code key}.
|
|
* @returns {void}
|
|
*/
|
|
setItem(key, value) {
|
|
value = String(value); // eslint-disable-line no-param-reassign
|
|
this[key] = value;
|
|
typeof this._keyPrefix === 'undefined'
|
|
|| AsyncStorage.setItem(`${String(this._keyPrefix)}${key}`, value);
|
|
}
|
|
}
|