diff options
| author | Pliable Pixels <pliablepixels@gmail.com> | 2017-09-21 12:49:18 -0400 |
|---|---|---|
| committer | Pliable Pixels <pliablepixels@gmail.com> | 2017-09-21 12:49:18 -0400 |
| commit | b28028ac4082842143b0f528d6bc539da6ccb419 (patch) | |
| tree | 1e26ea969a781ed8e323fca4e3c76345113fc694 /www/lib/localforage/src/drivers | |
| parent | 676270d21beed31d767a06c89522198c77d5d865 (diff) | |
mega changes, including updates and X
Diffstat (limited to 'www/lib/localforage/src/drivers')
| -rw-r--r-- | www/lib/localforage/src/drivers/indexeddb.js | 689 | ||||
| -rw-r--r-- | www/lib/localforage/src/drivers/localstorage.js | 270 | ||||
| -rw-r--r-- | www/lib/localforage/src/drivers/websql.js | 351 |
3 files changed, 1310 insertions, 0 deletions
diff --git a/www/lib/localforage/src/drivers/indexeddb.js b/www/lib/localforage/src/drivers/indexeddb.js new file mode 100644 index 00000000..2dd52554 --- /dev/null +++ b/www/lib/localforage/src/drivers/indexeddb.js @@ -0,0 +1,689 @@ +import createBlob from '../utils/createBlob'; +import idb from '../utils/idb'; +import Promise from '../utils/promise'; +import executeCallback from '../utils/executeCallback'; + +// Some code originally from async_storage.js in +// [Gaia](https://github.com/mozilla-b2g/gaia). + +var DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support'; +var supportsBlobs; +var dbContexts; + +// Transform a binary string to an array buffer, because otherwise +// weird stuff happens when you try to work with the binary string directly. +// It is known. +// From http://stackoverflow.com/questions/14967647/ (continues on next line) +// encode-decode-image-with-base64-breaks-image (2013-04-21) +function _binStringToArrayBuffer(bin) { + var length = bin.length; + var buf = new ArrayBuffer(length); + var arr = new Uint8Array(buf); + for (var i = 0; i < length; i++) { + arr[i] = bin.charCodeAt(i); + } + return buf; +} + +// +// Blobs are not supported in all versions of IndexedDB, notably +// Chrome <37 and Android <5. In those versions, storing a blob will throw. +// +// Various other blob bugs exist in Chrome v37-42 (inclusive). +// Detecting them is expensive and confusing to users, and Chrome 37-42 +// is at very low usage worldwide, so we do a hacky userAgent check instead. +// +// content-type bug: https://code.google.com/p/chromium/issues/detail?id=408120 +// 404 bug: https://code.google.com/p/chromium/issues/detail?id=447916 +// FileReader bug: https://code.google.com/p/chromium/issues/detail?id=447836 +// +// Code borrowed from PouchDB. See: +// https://github.com/pouchdb/pouchdb/blob/9c25a23/src/adapters/idb/blobSupport.js +// +function _checkBlobSupportWithoutCaching(txn) { + return new Promise(function(resolve) { + var blob = createBlob(['']); + txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key'); + + txn.onabort = function(e) { + // If the transaction aborts now its due to not being able to + // write to the database, likely due to the disk being full + e.preventDefault(); + e.stopPropagation(); + resolve(false); + }; + + txn.oncomplete = function() { + var matchedChrome = navigator.userAgent.match(/Chrome\/(\d+)/); + var matchedEdge = navigator.userAgent.match(/Edge\//); + // MS Edge pretends to be Chrome 42: + // https://msdn.microsoft.com/en-us/library/hh869301%28v=vs.85%29.aspx + resolve(matchedEdge || !matchedChrome || + parseInt(matchedChrome[1], 10) >= 43); + }; + }).catch(function() { + return false; // error, so assume unsupported + }); +} + +function _checkBlobSupport(idb) { + if (typeof supportsBlobs === 'boolean') { + return Promise.resolve(supportsBlobs); + } + return _checkBlobSupportWithoutCaching(idb).then(function(value) { + supportsBlobs = value; + return supportsBlobs; + }); +} + +function _deferReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Create a deferred object representing the current database operation. + var deferredOperation = {}; + + deferredOperation.promise = new Promise(function(resolve) { + deferredOperation.resolve = resolve; + }); + + // Enqueue the deferred operation. + dbContext.deferredOperations.push(deferredOperation); + + // Chain its promise to the database readiness. + if (!dbContext.dbReady) { + dbContext.dbReady = deferredOperation.promise; + } else { + dbContext.dbReady = dbContext.dbReady.then(function() { + return deferredOperation.promise; + }); + } +} + +function _advanceReadiness(dbInfo) { + var dbContext = dbContexts[dbInfo.name]; + + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); + + // Resolve its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.resolve(); + } +} + +function _getConnection(dbInfo, upgradeNeeded) { + return new Promise(function(resolve, reject) { + + if (dbInfo.db) { + if (upgradeNeeded) { + _deferReadiness(dbInfo); + dbInfo.db.close(); + } else { + return resolve(dbInfo.db); + } + } + + var dbArgs = [dbInfo.name]; + + if (upgradeNeeded) { + dbArgs.push(dbInfo.version); + } + + var openreq = idb.open.apply(idb, dbArgs); + + if (upgradeNeeded) { + openreq.onupgradeneeded = function(e) { + var db = openreq.result; + try { + db.createObjectStore(dbInfo.storeName); + if (e.oldVersion <= 1) { + // Added when support for blob shims was added + db.createObjectStore(DETECT_BLOB_SUPPORT_STORE); + } + } catch (ex) { + if (ex.name === 'ConstraintError') { + console.warn('The database "' + dbInfo.name + '"' + + ' has been upgraded from version ' + e.oldVersion + + ' to version ' + e.newVersion + + ', but the storage "' + dbInfo.storeName + '" already exists.'); + } else { + throw ex; + } + } + }; + } + + openreq.onerror = function() { + reject(openreq.error); + }; + + openreq.onsuccess = function() { + resolve(openreq.result); + _advanceReadiness(dbInfo); + }; + }); +} + +function _getOriginalConnection(dbInfo) { + return _getConnection(dbInfo, false); +} + +function _getUpgradedConnection(dbInfo) { + return _getConnection(dbInfo, true); +} + +function _isUpgradeNeeded(dbInfo, defaultVersion) { + if (!dbInfo.db) { + return true; + } + + var isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName); + var isDowngrade = dbInfo.version < dbInfo.db.version; + var isUpgrade = dbInfo.version > dbInfo.db.version; + + if (isDowngrade) { + // If the version is not the default one + // then warn for impossible downgrade. + if (dbInfo.version !== defaultVersion) { + console.warn('The database "' + dbInfo.name + '"' + + ' can\'t be downgraded from version ' + dbInfo.db.version + + ' to version ' + dbInfo.version + '.'); + } + // Align the versions to prevent errors. + dbInfo.version = dbInfo.db.version; + } + + if (isUpgrade || isNewStore) { + // If the store is new then increment the version (if needed). + // This will trigger an "upgradeneeded" event which is required + // for creating a store. + if (isNewStore) { + var incVersion = dbInfo.db.version + 1; + if (incVersion > dbInfo.version) { + dbInfo.version = incVersion; + } + } + + return true; + } + + return false; +} + +// encode a blob for indexeddb engines that don't support blobs +function _encodeBlob(blob) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onerror = reject; + reader.onloadend = function(e) { + var base64 = btoa(e.target.result || ''); + resolve({ + __local_forage_encoded_blob: true, + data: base64, + type: blob.type + }); + }; + reader.readAsBinaryString(blob); + }); +} + +// decode an encoded blob +function _decodeBlob(encodedBlob) { + var arrayBuff = _binStringToArrayBuffer(atob(encodedBlob.data)); + return createBlob([arrayBuff], {type: encodedBlob.type}); +} + +// is this one of our fancy encoded blobs? +function _isEncodedBlob(value) { + return value && value.__local_forage_encoded_blob; +} + +// Specialize the default `ready()` function by making it dependent +// on the current database operations. Thus, the driver will be actually +// ready when it's been initialized (default) *and* there are no pending +// operations on the database (initiated by some other instances). +function _fullyReady(callback) { + var self = this; + + var promise = self._initReady().then(function() { + var dbContext = dbContexts[self._dbInfo.name]; + + if (dbContext && dbContext.dbReady) { + return dbContext.dbReady; + } + }); + + promise.then(callback, callback); + return promise; +} + +// 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]; + } + } + + // Initialize a singleton container for all running localForages. + if (!dbContexts) { + dbContexts = {}; + } + + // Get the current context of the database; + var dbContext = dbContexts[dbInfo.name]; + + // ...or create a new context. + if (!dbContext) { + dbContext = { + // Running localForages sharing a database. + forages: [], + // Shared database. + db: null, + // Database readiness (promise). + dbReady: null, + // Deferred operations on the database. + deferredOperations: [] + }; + // Register the new context in the global container. + dbContexts[dbInfo.name] = dbContext; + } + + // Register itself as a running localForage in the current context. + dbContext.forages.push(self); + + // Replace the default `ready()` function with the specialized one. + if (!self._initReady) { + self._initReady = self.ready; + self.ready = _fullyReady; + } + + // Create an array of initialization states of the related localForages. + var initPromises = []; + + function ignoreErrors() { + // Don't handle errors here, + // just makes sure related localForages aren't pending. + return Promise.resolve(); + } + + for (var j = 0; j < dbContext.forages.length; j++) { + var forage = dbContext.forages[j]; + if (forage !== self) { // Don't wait for itself... + initPromises.push(forage._initReady().catch(ignoreErrors)); + } + } + + // Take a snapshot of the related localForages. + var forages = dbContext.forages.slice(0); + + // Initialize the connection process only when + // all the related localForages aren't pending. + return Promise.all(initPromises).then(function() { + dbInfo.db = dbContext.db; + // Get the connection or open a new one without upgrade. + return _getOriginalConnection(dbInfo); + }).then(function(db) { + dbInfo.db = db; + if (_isUpgradeNeeded(dbInfo, self._defaultConfig.version)) { + // Reopen the database for upgrading. + return _getUpgradedConnection(dbInfo); + } + return db; + }).then(function(db) { + dbInfo.db = dbContext.db = db; + self._dbInfo = dbInfo; + // Share the final connection amongst related localForages. + for (var k = 0; k < forages.length; k++) { + var forage = forages[k]; + if (forage !== self) { // Self is already up-to-date. + forage._dbInfo.db = dbInfo.db; + forage._dbInfo.version = dbInfo.version; + } + } + }); +} + +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 = 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; + } + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + resolve(value); + }; + + req.onerror = function() { + reject(req.error); + }; + }).catch(reject); + }); + + executeCallback(promise, callback); + return promise; +} + +// Iterate over all items stored in database. +function iterate(iterator, 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 iterationNumber = 1; + + req.onsuccess = function() { + var cursor = req.result; + + if (cursor) { + var value = cursor.value; + if (_isEncodedBlob(value)) { + value = _decodeBlob(value); + } + var result = iterator(value, cursor.key, + iterationNumber++); + + if (result !== void(0)) { + resolve(result); + } else { + cursor.continue(); + } + } else { + resolve(); + } + }; + + req.onerror = function() { + reject(req.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') { + console.warn(key + + ' used as a key, but it is not a string.'); + key = String(key); + } + + var promise = new Promise(function(resolve, reject) { + var dbInfo; + self.ready().then(function() { + dbInfo = self._dbInfo; + if (value instanceof Blob) { + return _checkBlobSupport(dbInfo.db).then(function(blobSupport) { + if (blobSupport) { + return value; + } + return _encodeBlob(value); + }); + } + return value; + }).then(function(value) { + var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.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; + } + + transaction.oncomplete = 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); + }; + transaction.onabort = transaction.onerror = function() { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + + var req = store.put(value, key); + }).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') { + 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 transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.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); + transaction.oncomplete = function() { + resolve(); + }; + + transaction.onerror = function() { + reject(req.error); + }; + + // The request will be also be aborted if we've exceeded our storage + // space. + transaction.onabort = function() { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + }).catch(reject); + }); + + executeCallback(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 transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); + var store = transaction.objectStore(dbInfo.storeName); + var req = store.clear(); + + transaction.oncomplete = function() { + resolve(); + }; + + transaction.onabort = transaction.onerror = function() { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + }).catch(reject); + }); + + executeCallback(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; +} + +var asyncStorage = { + _driver: 'asyncStorage', + _initStorage: _initStorage, + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys +}; +export default asyncStorage; diff --git a/www/lib/localforage/src/drivers/localstorage.js b/www/lib/localforage/src/drivers/localstorage.js new file mode 100644 index 00000000..85ef32bf --- /dev/null +++ b/www/lib/localforage/src/drivers/localstorage.js @@ -0,0 +1,270 @@ +// 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; diff --git a/www/lib/localforage/src/drivers/websql.js b/www/lib/localforage/src/drivers/websql.js new file mode 100644 index 00000000..04e6df2d --- /dev/null +++ b/www/lib/localforage/src/drivers/websql.js @@ -0,0 +1,351 @@ +import serializer from '../utils/serializer'; +import Promise from '../utils/promise'; +import executeCallback from '../utils/executeCallback'; + +/* + * Includes code from: + * + * base64-arraybuffer + * https://github.com/niklasvh/base64-arraybuffer + * + * Copyright (c) 2012 Niklas von Hertzen + * Licensed under the MIT license. + */ +// 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]; + } + } + + var dbInfoPromise = 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 reject(e); + } + + // 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); + }); + }); + }); + + dbInfo.serializer = serializer; + return dbInfoPromise; +} + +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 = 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 = dbInfo.serializer.deserialize(result); + } + + resolve(result); + }, function(t, error) { + + reject(error); + }); + }); + }).catch(reject); + }); + + executeCallback(promise, callback); + return promise; +} + +function iterate(iterator, 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 * FROM ' + dbInfo.storeName, [], + function(t, results) { + var rows = results.rows; + var length = rows.length; + + for (var i = 0; i < length; i++) { + var item = rows.item(i); + var result = item.value; + + // Check to see if this is serialized content + // we need to unpack. + if (result) { + result = dbInfo.serializer.deserialize(result); + } + + result = iterator(result, item.key, i + 1); + + // void(0) prevents problems with redefinition + // of `undefined`. + if (result !== void(0)) { + resolve(result); + return; + } + } + + resolve(); + }, 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') { + 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; + + var dbInfo = self._dbInfo; + dbInfo.serializer.serialize(value, function(value, error) { + if (error) { + reject(error); + } else { + 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') { + 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; +} + +var webSQLStorage = { + _driver: 'webSQLStorage', + _initStorage: _initStorage, + iterate: iterate, + getItem: getItem, + setItem: setItem, + removeItem: removeItem, + clear: clear, + length: length, + key: key, + keys: keys +}; + +export default webSQLStorage; |
