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}. *

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