UNPKG

localforage

Version:
1,407 lines (1,211 loc) 59.6 kB
/*! localForage -- Offline Storage, Improved Version 1.0.2 http://mozilla.github.io/localForage (c) 2013-2014 Mozilla, Apache License 2.0 */ // Some code originally from async_storage.js in // [Gaia](https://github.com/mozilla-b2g/gaia). (function() { 'use strict'; // Originally found in https://github.com/mozilla-b2g/gaia/blob/e8f624e4cc9ea945727278039b3bc9bcb9f8667a/shared/js/async_storage.js // Promises! var Promise = (typeof module !== 'undefined' && module.exports) ? require('promise') : this.Promise; // Initialize IndexedDB; fall back to vendor-prefixed versions if needed. var indexedDB = indexedDB || this.indexedDB || this.webkitIndexedDB || this.mozIndexedDB || this.OIndexedDB || this.msIndexedDB; // If IndexedDB isn't available, we get outta here! if (!indexedDB) { return; } // Open the IndexedDB database (automatically creates one if one didn't // previously exist), using any options set in the config. function _initStorage(options) { var self = this; var dbInfo = { db: null }; if (options) { for (var i in options) { dbInfo[i] = options[i]; } } return new Promise(function(resolve, reject) { var openreq = indexedDB.open(dbInfo.name, dbInfo.version); openreq.onerror = function() { reject(openreq.error); }; openreq.onupgradeneeded = function() { // First time setup: create an empty object store openreq.result.createObjectStore(dbInfo.storeName); }; openreq.onsuccess = function() { dbInfo.db = openreq.result; self._dbInfo = dbInfo; resolve(); }; }); } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly') .objectStore(dbInfo.storeName); var req = store.get(key); req.onsuccess = function() { var value = req.result; if (value === undefined) { value = null; } resolve(value); }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeDeferedCallback(promise, callback); return promise; } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readwrite') .objectStore(dbInfo.storeName); // The reason we don't _save_ null is because IE 10 does // not support saving the `null` type in IndexedDB. How // ironic, given the bug below! // See: https://github.com/mozilla/localForage/issues/161 if (value === null) { value = undefined; } var req = store.put(value, key); req.onsuccess = function() { // Cast to undefined so the value passed to // callback/promise is the same as what one would get out // of `getItem()` later. This leads to some weirdness // (setItem('foo', undefined) will return `null`), but // it's not my fault localStorage is our baseline and that // it's weird. if (value === undefined) { value = null; } resolve(value); }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeDeferedCallback(promise, callback); return promise; } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readwrite') .objectStore(dbInfo.storeName); // We use a Grunt task to make this safe for IE and some // versions of Android (including those used by Cordova). // Normally IE won't like `.delete()` and will insist on // using `['delete']()`, but we have a build step that // fixes this for us now. var req = store["delete"](key); req.onsuccess = function() { resolve(); }; req.onerror = function() { reject(req.error); }; // The request will be aborted if we've exceeded our storage // space. In this case, we will reject with a specific // "QuotaExceededError". req.onabort = function(event) { var error = event.target.error; if (error === 'QuotaExceededError') { reject(error); } }; })["catch"](reject); }); executeDeferedCallback(promise, callback); return promise; } function clear(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readwrite') .objectStore(dbInfo.storeName); var req = store.clear(); req.onsuccess = function() { resolve(); }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeDeferedCallback(promise, callback); return promise; } function length(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly') .objectStore(dbInfo.storeName); var req = store.count(); req.onsuccess = function() { resolve(req.result); }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeCallback(promise, callback); return promise; } function key(n, callback) { var self = this; var promise = new Promise(function(resolve, reject) { if (n < 0) { resolve(null); return; } self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly') .objectStore(dbInfo.storeName); var advanced = false; var req = store.openCursor(); req.onsuccess = function() { var cursor = req.result; if (!cursor) { // this means there weren't enough keys resolve(null); return; } if (n === 0) { // We have the first key, return it if that's what they // wanted. resolve(cursor.key); } else { if (!advanced) { // Otherwise, ask the cursor to skip ahead n // records. advanced = true; cursor.advance(n); } else { // When we get here, we've got the nth key. resolve(cursor.key); } } }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeCallback(promise, callback); return promise; } function keys(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly') .objectStore(dbInfo.storeName); var req = store.openCursor(); var keys = []; req.onsuccess = function() { var cursor = req.result; if (!cursor) { resolve(keys); return; } keys.push(cursor.key); cursor["continue"](); }; req.onerror = function() { reject(req.error); }; })["catch"](reject); }); executeCallback(promise, callback); return promise; } function executeCallback(promise, callback) { if (callback) { promise.then(function(result) { callback(null, result); }, function(error) { callback(error); }); } } function executeDeferedCallback(promise, callback) { if (callback) { promise.then(function(result) { deferCallback(callback, result); }, function(error) { callback(error); }); } } // Under Chrome the callback is called before the changes (save, clear) // are actually made. So we use a defer function which wait that the // call stack to be empty. // For more info : https://github.com/mozilla/localForage/issues/175 // Pull request : https://github.com/mozilla/localForage/pull/178 function deferCallback(callback, result) { if (callback) { return setTimeout(function() { return callback(null, result); }, 0); } } var asyncStorage = { _driver: 'asyncStorage', _initStorage: _initStorage, getItem: getItem, setItem: setItem, removeItem: removeItem, clear: clear, length: length, key: key, keys: keys }; if (typeof define === 'function' && define.amd) { define('asyncStorage', function() { return asyncStorage; }); } else if (typeof module !== 'undefined' && module.exports) { module.exports = asyncStorage; } else { this.asyncStorage = asyncStorage; } }).call(window); // 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). (function() { 'use strict'; // Promises! var Promise = (typeof module !== 'undefined' && module.exports) ? require('promise') : this.Promise; var localStorage = null; // If the app is running inside a Google Chrome packaged webapp, or some // other context where localStorage isn't available, we don't use // localStorage. This feature detection is preferred over the old // `if (window.chrome && window.chrome.runtime)` code. // See: https://github.com/mozilla/localForage/issues/68 try { // If localStorage isn't available, we get outta here! // This should be inside a try catch if (!this.localStorage || !('setItem' in this.localStorage)) { return; } // Initialize localStorage and create a variable to use throughout // the code. localStorage = this.localStorage; } catch (e) { return; } // 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 + '/'; self._dbInfo = dbInfo; return Promise.resolve(); } var SERIALIZED_MARKER = '__lfsc__:'; var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length; // OMG the serializations! var TYPE_ARRAYBUFFER = 'arbf'; var TYPE_BLOB = 'blob'; var TYPE_INT8ARRAY = 'si08'; var TYPE_UINT8ARRAY = 'ui08'; var TYPE_UINT8CLAMPEDARRAY = 'uic8'; var TYPE_INT16ARRAY = 'si16'; var TYPE_INT32ARRAY = 'si32'; var TYPE_UINT16ARRAY = 'ur16'; var TYPE_UINT32ARRAY = 'ui32'; var TYPE_FLOAT32ARRAY = 'fl32'; var TYPE_FLOAT64ARRAY = 'fl64'; var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length; // 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 = new Promise(function(resolve, reject) { 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); } } resolve(); })["catch"](reject); }); 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { try { 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 = _deserialize(result); } resolve(result); } catch (e) { reject(e); } })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Same as localStorage's key() method, except takes a callback. function key(n, callback) { var self = this; var promise = new Promise(function(resolve, reject) { 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); } resolve(result); })["catch"](reject); }); executeCallback(promise, callback); return promise; } function keys(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; var length = localStorage.length; var keys = []; for (var i = 0; i < length; i++) { keys.push(localStorage.key(i) .substring(dbInfo.keyPrefix.length)); } resolve(keys); })["catch"](reject); }); 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 = new Promise(function(resolve, reject) { self.ready().then(function() { var result = localStorage.length; resolve(result); })["catch"](reject); }); 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; localStorage.removeItem(dbInfo.keyPrefix + key); resolve(); })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Deserialize data we've inserted into a value column/field. We place // special markers into our strings to mark them as encoded; this isn't // as nice as a meta field, but it's the only sane thing we can do whilst // keeping localStorage support intact. // // Oftentimes this will just deserialize JSON content, but if we have a // special marker (SERIALIZED_MARKER, defined above), we will extract // some kind of arraybuffer/binary data/typed array out of the string. function _deserialize(value) { // If we haven't marked this string as being specially serialized (i.e. // something other than serialized JSON), we can just return it and be // done with it. if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) { return JSON.parse(value); } // The following code deals with deserializing some kind of Blob or // TypedArray. First we separate out the type of data we're dealing // with from the data itself. var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH); var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH); // Fill the string into a ArrayBuffer. // 2 bytes for each char. var buffer = new ArrayBuffer(serializedString.length * 2); var bufferView = new Uint16Array(buffer); for (var i = serializedString.length - 1; i >= 0; i--) { bufferView[i] = serializedString.charCodeAt(i); } // Return the right type based on the code/type set during // serialization. switch (type) { case TYPE_ARRAYBUFFER: return buffer; case TYPE_BLOB: return new Blob([buffer]); case TYPE_INT8ARRAY: return new Int8Array(buffer); case TYPE_UINT8ARRAY: return new Uint8Array(buffer); case TYPE_UINT8CLAMPEDARRAY: return new Uint8ClampedArray(buffer); case TYPE_INT16ARRAY: return new Int16Array(buffer); case TYPE_UINT16ARRAY: return new Uint16Array(buffer); case TYPE_INT32ARRAY: return new Int32Array(buffer); case TYPE_UINT32ARRAY: return new Uint32Array(buffer); case TYPE_FLOAT32ARRAY: return new Float32Array(buffer); case TYPE_FLOAT64ARRAY: return new Float64Array(buffer); default: throw new Error('Unkown type: ' + type); } } // Converts a buffer to a string to store, serialized, in the backend // storage library. function _bufferToString(buffer) { var str = ''; var uint16Array = new Uint16Array(buffer); try { str = String.fromCharCode.apply(null, uint16Array); } catch (e) { // This is a fallback implementation in case the first one does // not work. This is required to get the phantomjs passing... for (var i = 0; i < uint16Array.length; i++) { str += String.fromCharCode(uint16Array[i]); } } return str; } // Serialize a value, afterwards executing a callback (which usually // instructs the `setItem()` callback/promise to be executed). This is how // we store binary data with localStorage. function _serialize(value, callback) { var valueString = ''; if (value) { valueString = value.toString(); } // Cannot use `value instanceof ArrayBuffer` or such here, as these // checks fail when running the tests using casper.js... // // TODO: See why those tests fail and use a better solution. if (value && (value.toString() === '[object ArrayBuffer]' || value.buffer && value.buffer.toString() === '[object ArrayBuffer]')) { // Convert binary arrays to a string and prefix the string with // a special marker. var buffer; var marker = SERIALIZED_MARKER; if (value instanceof ArrayBuffer) { buffer = value; marker += TYPE_ARRAYBUFFER; } else { buffer = value.buffer; if (valueString === '[object Int8Array]') { marker += TYPE_INT8ARRAY; } else if (valueString === '[object Uint8Array]') { marker += TYPE_UINT8ARRAY; } else if (valueString === '[object Uint8ClampedArray]') { marker += TYPE_UINT8CLAMPEDARRAY; } else if (valueString === '[object Int16Array]') { marker += TYPE_INT16ARRAY; } else if (valueString === '[object Uint16Array]') { marker += TYPE_UINT16ARRAY; } else if (valueString === '[object Int32Array]') { marker += TYPE_INT32ARRAY; } else if (valueString === '[object Uint32Array]') { marker += TYPE_UINT32ARRAY; } else if (valueString === '[object Float32Array]') { marker += TYPE_FLOAT32ARRAY; } else if (valueString === '[object Float64Array]') { marker += TYPE_FLOAT64ARRAY; } else { callback(new Error('Failed to get type for BinaryArray')); } } callback(marker + _bufferToString(buffer)); } else if (valueString === '[object Blob]') { // Conver the blob to a binaryArray and then to a string. var fileReader = new FileReader(); fileReader.onload = function() { var str = _bufferToString(this.result); callback(SERIALIZED_MARKER + TYPE_BLOB + str); }; fileReader.readAsArrayBuffer(value); } else { try { callback(JSON.stringify(value)); } catch (e) { window.console.error("Couldn't convert value into a JSON " + 'string: ', value); callback(e); } } } // 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { 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; _serialize(value, function(value, error) { if (error) { reject(error); } else { try { var dbInfo = self._dbInfo; localStorage.setItem(dbInfo.keyPrefix + key, value); } 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); } } resolve(originalValue); } }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } function executeCallback(promise, callback) { if (callback) { promise.then(function(result) { callback(null, result); }, function(error) { callback(error); }); } } var localStorageWrapper = { _driver: 'localStorageWrapper', _initStorage: _initStorage, // Default API, from Gaia/localStorage. getItem: getItem, setItem: setItem, removeItem: removeItem, clear: clear, length: length, key: key, keys: keys }; if (typeof define === 'function' && define.amd) { define('localStorageWrapper', function() { return localStorageWrapper; }); } else if (typeof module !== 'undefined' && module.exports) { module.exports = localStorageWrapper; } else { this.localStorageWrapper = localStorageWrapper; } }).call(window); /* * Includes code from: * * base64-arraybuffer * https://github.com/niklasvh/base64-arraybuffer * * Copyright (c) 2012 Niklas von Hertzen * Licensed under the MIT license. */ (function() { 'use strict'; // Sadly, the best way to save binary data in WebSQL is Base64 serializing // it, so this is how we store it to prevent very strange errors with less // verbose ways of binary <-> string data storage. var BASE_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Promises! var Promise = (typeof module !== 'undefined' && module.exports) ? require('promise') : this.Promise; var openDatabase = this.openDatabase; var SERIALIZED_MARKER = '__lfsc__:'; var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length; // OMG the serializations! var TYPE_ARRAYBUFFER = 'arbf'; var TYPE_BLOB = 'blob'; var TYPE_INT8ARRAY = 'si08'; var TYPE_UINT8ARRAY = 'ui08'; var TYPE_UINT8CLAMPEDARRAY = 'uic8'; var TYPE_INT16ARRAY = 'si16'; var TYPE_INT32ARRAY = 'si32'; var TYPE_UINT16ARRAY = 'ur16'; var TYPE_UINT32ARRAY = 'ui32'; var TYPE_FLOAT32ARRAY = 'fl32'; var TYPE_FLOAT64ARRAY = 'fl64'; var TYPE_SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length; // If WebSQL methods aren't available, we can stop now. if (!openDatabase) { return; } // Open the WebSQL database (automatically creates one if one didn't // previously exist), using any options set in the config. function _initStorage(options) { var self = this; var dbInfo = { db: null }; if (options) { for (var i in options) { dbInfo[i] = typeof(options[i]) !== 'string' ? options[i].toString() : options[i]; } } return new Promise(function(resolve, reject) { // Open the database; the openDatabase API will automatically // create it for us if it doesn't exist. try { dbInfo.db = openDatabase(dbInfo.name, String(dbInfo.version), dbInfo.description, dbInfo.size); } catch (e) { return self.setDriver("localStorageWrapper").then(function() { return self._initStorage(options); }).then(resolve)["catch"](reject); } // Create our key/value table if it doesn't exist. dbInfo.db.transaction(function(t) { t.executeSql('CREATE TABLE IF NOT EXISTS ' + dbInfo.storeName + ' (id INTEGER PRIMARY KEY, key unique, value)', [], function() { self._dbInfo = dbInfo; resolve(); }, function(t, error) { reject(error); }); }); }); } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('SELECT * FROM ' + dbInfo.storeName + ' WHERE key = ? LIMIT 1', [key], function(t, results) { var result = results.rows.length ? results.rows.item(0).value : null; // Check to see if this is serialized content we need to // unpack. if (result) { result = _deserialize(result); } resolve(result); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { // The localStorage API doesn't return undefined values in an // "expected" way, so undefined is always cast to null in all // drivers. See: https://github.com/mozilla/localForage/pull/42 if (value === undefined) { value = null; } // Save the original value to pass to the callback. var originalValue = value; _serialize(value, function(value, error) { if (error) { reject(error); } else { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('INSERT OR REPLACE INTO ' + dbInfo.storeName + ' (key, value) VALUES (?, ?)', [key, value], function() { resolve(originalValue); }, function(t, error) { reject(error); }); }, function(sqlError) { // The transaction failed; check // to see if it's a quota error. if (sqlError.code === sqlError.QUOTA_ERR) { // We reject the callback outright for now, but // it's worth trying to re-run the transaction. // Even if the user accepts the prompt to use // more storage on Safari, this error will // be called. // // TODO: Try to re-run the transaction. reject(sqlError); } }); } }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } 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') { window.console.warn(key + ' used as a key, but it is not a string.'); key = String(key); } var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('DELETE FROM ' + dbInfo.storeName + ' WHERE key = ?', [key], function() { resolve(); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Deletes every item in the table. // TODO: Find out if this resets the AUTO_INCREMENT number. function clear(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('DELETE FROM ' + dbInfo.storeName, [], function() { resolve(); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Does a simple `COUNT(key)` to get the number of items stored in // localForage. function length(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { // Ahhh, SQL makes this one soooooo easy. t.executeSql('SELECT COUNT(key) as c FROM ' + dbInfo.storeName, [], function(t, results) { var result = results.rows.item(0).c; resolve(result); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Return the key located at key index X; essentially gets the key from a // `WHERE id = ?`. This is the most efficient way I can think to implement // this rarely-used (in my experience) part of the API, but it can seem // inconsistent, because we do `INSERT OR REPLACE INTO` on `setItem()`, so // the ID of each key will change every time it's updated. Perhaps a stored // procedure for the `setItem()` SQL would solve this problem? // TODO: Don't change ID on `setItem()`. function key(n, callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('SELECT key FROM ' + dbInfo.storeName + ' WHERE id = ? LIMIT 1', [n + 1], function(t, results) { var result = results.rows.length ? results.rows.item(0).key : null; resolve(result); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } function keys(callback) { var self = this; var promise = new Promise(function(resolve, reject) { self.ready().then(function() { var dbInfo = self._dbInfo; dbInfo.db.transaction(function(t) { t.executeSql('SELECT key FROM ' + dbInfo.storeName, [], function(t, results) { var keys = []; for (var i = 0; i < results.rows.length; i++) { keys.push(results.rows.item(i).key); } resolve(keys); }, function(t, error) { reject(error); }); }); })["catch"](reject); }); executeCallback(promise, callback); return promise; } // Converts a buffer to a string to store, serialized, in the backend // storage library. function _bufferToString(buffer) { // base64-arraybuffer var bytes = new Uint8Array(buffer); var i; var base64String = ''; for (i = 0; i < bytes.length; i += 3) { /*jslint bitwise: true */ base64String += BASE_CHARS[bytes[i] >> 2]; base64String += BASE_CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; base64String += BASE_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; base64String += BASE_CHARS[bytes[i + 2] & 63]; } if ((bytes.length % 3) === 2) { base64String = base64String.substring(0, base64String.length - 1) + '='; } else if (bytes.length % 3 === 1) { base64String = base64String.substring(0, base64String.length - 2) + '=='; } return base64String; } // Deserialize data we've inserted into a value column/field. We place // special markers into our strings to mark them as encoded; this isn't // as nice as a meta field, but it's the only sane thing we can do whilst // keeping localStorage support intact. // // Oftentimes this will just deserialize JSON content, but if we have a // special marker (SERIALIZED_MARKER, defined above), we will extract // some kind of arraybuffer/binary data/typed array out of the string. function _deserialize(value) { // If we haven't marked this string as being specially serialized (i.e. // something other than serialized JSON), we can just return it and be // done with it. if (value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER) { return JSON.parse(value); } // The following code deals with deserializing some kind of Blob or // TypedArray. First we separate out the type of data we're dealing // with from the data itself. var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH); var type = value.substring(SERIALIZED_MARKER_LENGTH, TYPE_SERIALIZED_MARKER_LENGTH); // Fill the string into a ArrayBuffer. var bufferLength = serializedString.length * 0.75; var len = serializedString.length; var i; var p = 0; var encoded1, encoded2, encoded3, encoded4; if (serializedString[serializedString.length - 1] === '=') { bufferLength--; if (serializedString[serializedString.length - 2] === '=') { bufferLength--; } } var buffer = new ArrayBuffer(bufferLength); var bytes = new Uint8Array(buffer); for (i = 0; i < len; i+=4) { encoded1 = BASE_CHARS.indexOf(serializedString[i]); encoded2 = BASE_CHARS.indexOf(serializedString[i+1]); encoded3 = BASE_CHARS.indexOf(serializedString[i+2]); encoded4 = BASE_CHARS.indexOf(serializedString[i+3]); /*jslint bitwise: true */ bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); } // Return the right type based on the code/type set during // serialization. switch (type) { case TYPE_ARRAYBUFFER: return buffer; case TYPE_BLOB: return new Blob([buffer]); case TYPE_INT8ARRAY: return new Int8Array(buffer); case TYPE_UINT8ARRAY: return new Uint8Array(buffer); case TYPE_UINT8CLAMPEDARRAY: return new Uint8ClampedArray(buffer); case TYPE_INT16ARRAY: return new Int16Array(buffer); case TYPE_UINT16ARRAY: return new Uint16Array(buffer); case TYPE_INT32ARRAY: return new Int32Array(buffer); case TYPE_UINT32ARRAY: return new Uint32Array(buffer); case TYPE_FLOAT32ARRAY: return new Float32Array(buffer); case TYPE_FLOAT64ARRAY: return new Float64Array(buffer); default: throw new Error('Unkown type: ' + type); } } // Serialize a value, afterwards executing a callback (which usually // instructs the `setItem()` callback/promise to be executed). This is how // we store binary data with localStorage. function _serialize(value, callback) { var valueString = ''; if (value) { valueString = value.toString(); } // Cannot use `value instanceof ArrayBuffer` or such here, as these // checks fail when running the tests using casper.js... // // TODO: See why those tests fail and use a better solution. if (value && (value.toString() === '[object ArrayBuffer]' || value.buffer && value.buffer.toString() === '[object ArrayBuffer]')) { // Convert binary arrays to a string and prefix the string with // a special marker. var buffer; var marker = SERIALIZED_MARKER; if (value instanceof ArrayBuffer) { buffer = value; marker += TYPE_ARRAYBUFFER; } else { buffer = value.buffer; if (valueString === '[object Int8Array]') { marker += TYPE_INT8ARRAY; } else if (valueString === '[object Uint8Array]') { marker += TYPE_UINT8ARRAY; } else if (valueString === '[object Uint8ClampedArray]') { marker += TYPE_UINT8CLAMPEDARRAY; } else if (valueString === '[object Int16Array]') { marker += TYPE_INT16ARRAY; } else if (valueString === '[object Uint16Array]') { marker += TYPE_UINT16ARRAY; } else if (valueString === '[object Int32Array]') { marker += TYPE_INT32ARRAY; } else if (valueString === '[object Uint32Array]') { marker += TYPE_UINT32ARRAY; } else if (valueString === '[object Float32Array]') { marker += TYPE_FLOAT32ARRAY; } else if (valueString === '[object Float64Array]') { marker += TYPE_FLOAT64ARRAY; } else { callback(new Error('Failed to get type for BinaryArray')); } } callback(marker + _bufferToString(buffer)); } else if (valueString === '[object Blob]') { // Conver the blob to a binaryArray and then to a string. var fileReader = new FileReader(); fileReader.onload = function() { var str = _bufferToString(this.result); callback(SERIALIZED_MARKER + TYPE_BLOB + str); }; fileReader.readAsArrayBuffer(value); } else { try { callback(JSON.stringify(value)); } catch (e) { window.console.error("Couldn't convert value into a JSON " + 'string: ', value); callback(null, e); } } } function executeCallback(promise, callback) { if (callback) { promise.then(function(result) { callback(null, result); }, function(error) { callback(error); }); } } var webSQLStorage = { _driver: 'webSQLStorage', _initStorage: _initStorage, getItem: getItem, setItem: setItem, removeItem: removeItem, clear: clear, length: length, key: key, keys: keys }; if (typeof define === 'function' && define.amd) { define('webSQLStorage', function() { return webSQLStorage; }); } else if (typeof module !== 'undefined' && module.exports) { module.exports = webSQLStorage; } else { this.webSQLStorage = webSQLStorage; } }).call(window); (function() { 'use strict'; // Promises! var Promise = (typeof module !== 'undefined' && module.exports) ? require('promise') : this.Promise; var DriverType = { INDEXEDDB: 'asyncStorage', LOCALSTORAGE: 'localStorageWrapper', WEBSQL: 'webSQLStorage' }; var DefaultDriverOrder = [ DriverType.INDEXEDDB, DriverType.WEBSQL, DriverType.LOCALSTORAGE ]; var LibraryMethods = [ 'clear', 'getItem', 'key', 'keys', 'length', 'removeItem', 'setItem' ]; var ModuleType = { DEFINE: 1, EXPORT: 2, WINDOW: 3 }; var DefaultConfig = { description: '', name: 'localforage', // Default DB size is _JUST UNDER_ 5MB, as it's the highest size // we can use without a prompt. size: