// If IndexedDB isn't available, we'll fall back to localStorage. // Note that this will have considerable performance and storage // side-effects (all data will be serialized on save and only data that // can be converted to a string via `JSON.stringify()` will be saved). import serializer from '../utils/serializer'; import Promise from '../utils/promise'; import executeCallback from '../utils/executeCallback'; // Config the localStorage backend, using options set in the config. function _initStorage(options) { var self = this; var dbInfo = {}; if (options) { for (var i in options) { dbInfo[i] = options[i]; } } dbInfo.keyPrefix = dbInfo.name + '/'; if (dbInfo.storeName !== self._defaultConfig.storeName) { dbInfo.keyPrefix += dbInfo.storeName + '/'; } self._dbInfo = dbInfo; dbInfo.serializer = serializer; return Promise.resolve(); } // Remove all keys from the datastore, effectively destroying all data in // the app's key/value store! function clear(callback) { var self = this; var promise = self.ready().then(function() { var keyPrefix = self._dbInfo.keyPrefix; for (var i = localStorage.length - 1; i >= 0; i--) { var key = localStorage.key(i); if (key.indexOf(keyPrefix) === 0) { localStorage.removeItem(key); } } }); executeCallback(promise, callback); return promise; } // Retrieve an item from the store. Unlike the original async_storage // library in Gaia, we don't modify return values at all. If a key's value // is `undefined`, we pass that value to the callback function. function getItem(key, callback) { var self = this; // Cast the key to a string, as that's all we can set as a key. if (typeof key !== 'string') { console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = self.ready().then(function() { var dbInfo = self._dbInfo; var result = localStorage.getItem(dbInfo.keyPrefix + key); // If a result was found, parse it from the serialized // string into a JS object. If result isn't truthy, the key // is likely undefined and we'll pass it straight to the // callback. if (result) { result = dbInfo.serializer.deserialize(result); } return result; }); executeCallback(promise, callback); return promise; } // Iterate over all items in the store. function iterate(iterator, callback) { var self = this; var promise = self.ready().then(function() { var dbInfo = self._dbInfo; var keyPrefix = dbInfo.keyPrefix; var keyPrefixLength = keyPrefix.length; var length = localStorage.length; // We use a dedicated iterator instead of the `i` variable below // so other keys we fetch in localStorage aren't counted in // the `iterationNumber` argument passed to the `iterate()` // callback. // // See: github.com/mozilla/localForage/pull/435#discussion_r38061530 var iterationNumber = 1; for (var i = 0; i < length; i++) { var key = localStorage.key(i); if (key.indexOf(keyPrefix) !== 0) { continue; } var value = localStorage.getItem(key); // If a result was found, parse it from the serialized // string into a JS object. If result isn't truthy, the // key is likely undefined and we'll pass it straight // to the iterator. if (value) { value = dbInfo.serializer.deserialize(value); } value = iterator(value, key.substring(keyPrefixLength), iterationNumber++); if (value !== void(0)) { return value; } } }); executeCallback(promise, callback); return promise; } // Same as localStorage's key() method, except takes a callback. function key(n, callback) { var self = this; var promise = self.ready().then(function() { var dbInfo = self._dbInfo; var result; try { result = localStorage.key(n); } catch (error) { result = null; } // Remove the prefix from the key, if a key is found. if (result) { result = result.substring(dbInfo.keyPrefix.length); } return result; }); executeCallback(promise, callback); return promise; } function keys(callback) { var self = this; var promise = self.ready().then(function() { var dbInfo = self._dbInfo; var length = localStorage.length; var keys = []; for (var i = 0; i < length; i++) { if (localStorage.key(i).indexOf(dbInfo.keyPrefix) === 0) { keys.push(localStorage.key(i).substring(dbInfo.keyPrefix.length)); } } return keys; }); executeCallback(promise, callback); return promise; } // Supply the number of keys in the datastore to the callback function. function length(callback) { var self = this; var promise = self.keys().then(function(keys) { return keys.length; }); executeCallback(promise, callback); return promise; } // Remove an item from the store, nice and simple. function removeItem(key, callback) { var self = this; // Cast the key to a string, as that's all we can set as a key. if (typeof key !== 'string') { console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = self.ready().then(function() { var dbInfo = self._dbInfo; localStorage.removeItem(dbInfo.keyPrefix + key); }); executeCallback(promise, callback); return promise; } // Set a key's value and run an optional callback once the value is set. // Unlike Gaia's implementation, the callback function is passed the value, // in case you want to operate on that value only after you're sure it // saved, or something like that. function setItem(key, value, callback) { var self = this; // Cast the key to a string, as that's all we can set as a key. if (typeof key !== 'string') { console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = self.ready().then(function() { // Convert undefined values to null. // https://github.com/mozilla/localForage/pull/42 if (value === undefined) { value = null; } // Save the original value to pass to the callback. var originalValue = value; return new Promise(function(resolve, reject) { var dbInfo = self._dbInfo; dbInfo.serializer.serialize(value, function(value, error) { if (error) { reject(error); } else { try { localStorage.setItem(dbInfo.keyPrefix + key, value); resolve(originalValue); } catch (e) { // localStorage capacity exceeded. // TODO: Make this a specific error/event. if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { reject(e); } reject(e); } } }); }); }); executeCallback(promise, callback); return promise; } var localStorageWrapper = { _driver: 'localStorageWrapper', _initStorage: _initStorage, // Default API, from Gaia/localStorage. iterate: iterate, getItem: getItem, setItem: setItem, removeItem: removeItem, clear: clear, length: length, key: key, keys: keys }; export default localStorageWrapper;