From 557c364cc7a01f2c9ba0d17d16fee279eb0dcf89 Mon Sep 17 00:00:00 2001 From: stkao05 Date: Thu, 2 Mar 2017 01:58:36 +0800 Subject: [PATCH 01/10] Try re-establish IDBDatabase connection after encoutner InvalidStateError error --- src/drivers/indexeddb.js | 110 ++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 26 deletions(-) diff --git a/src/drivers/indexeddb.js b/src/drivers/indexeddb.js index 51f936d..b27d276 100644 --- a/src/drivers/indexeddb.js +++ b/src/drivers/indexeddb.js @@ -115,6 +115,19 @@ function _advanceReadiness(dbInfo) { } } +function _rejectReadiness(dbInfo, err) { + var dbContext = dbContexts[dbInfo.name]; + + // Dequeue a deferred operation. + var deferredOperation = dbContext.deferredOperations.pop(); + + // Reject its promise (which is part of the database readiness + // chain of promises). + if (deferredOperation) { + deferredOperation.reject(err); + } +} + function _getConnection(dbInfo, upgradeNeeded) { return new Promise(function(resolve, reject) { @@ -262,6 +275,47 @@ function _fullyReady(callback) { return promise; } +// Try to establish a new db connection to replace the +// current one which is broken (i.e. experiencing +// InvalidStateError while creating a transaction). +function _tryReconnect(dbInfo) { + _deferReadiness(dbInfo); + + var dbContext = dbContexts[dbInfo.name]; + var forages = dbContext.forages; + + for (var i = 0; i < forages.length; i++) { + forages[i]._dbInfo.db.close(); + forages[i]._dbInfo.db = null; + } + + return _getConnection(dbInfo, false).then(function(db) { + for (var j = 0; j < forages.length; j++) { + forages[j]._dbInfo.db = db; + } + }).catch(function(err) { + _rejectReadiness(dbInfo, err); + throw err; + }); +} + +function createTransaction(dbInfo, mode) { + try { + var tx = dbInfo.db.transaction(dbInfo.storeName, mode); + return Promise.resolve(tx); + } catch (err) { + if (err.name === 'InvalidStateError') { + return _tryReconnect(dbInfo).then(function() { + + var tx = dbInfo.db.transaction(dbInfo.storeName, mode); + return Promise.resolve(tx); + }); + } + + return Promise.reject(err); + } +} + // Open the IndexedDB database (automatically creates one if one didn't // previously exist), using any options set in the config. function _initStorage(options) { @@ -355,6 +409,7 @@ function _initStorage(options) { }); } + function getItem(key, callback) { var self = this; @@ -367,9 +422,9 @@ function getItem(key, callback) { 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); + return createTransaction(self._dbInfo, 'readonly'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.get(key); req.onsuccess = function() { @@ -399,10 +454,9 @@ function iterate(iterator, callback) { 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); - + return createTransaction(self._dbInfo, 'readonly'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.openCursor(); var iterationNumber = 1; @@ -462,8 +516,13 @@ function setItem(key, value, callback) { } return value; }).then(function(value) { - var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite'); - var store = transaction.objectStore(dbInfo.storeName); + return createTransaction(self._dbInfo, 'readwrite').then(function(transaction) { + return [transaction, value]; + }); + }).then(function(txAndValue) { + var transaction = txAndValue[0]; + var value = txAndValue[1]; + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.put(value, key); // The reason we don't _save_ null is because IE 10 does @@ -510,9 +569,9 @@ function removeItem(key, callback) { 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); + return createTransaction(self._dbInfo, 'readwrite'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); // We use a Grunt task to make this safe for IE and some // versions of Android (including those used by Cordova). @@ -546,9 +605,9 @@ function clear(callback) { 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); + return createTransaction(self._dbInfo, 'readwrite'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.clear(); transaction.oncomplete = function() { @@ -571,9 +630,9 @@ function length(callback) { 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); + return createTransaction(self._dbInfo, 'readonly'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.count(); req.onsuccess = function() { @@ -601,12 +660,12 @@ function key(n, callback) { } self.ready().then(function() { - var dbInfo = self._dbInfo; - var store = dbInfo.db.transaction(dbInfo.storeName, 'readonly') - .objectStore(dbInfo.storeName); - + return createTransaction(self._dbInfo, 'readonly'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var advanced = false; var req = store.openCursor(); + req.onsuccess = function() { var cursor = req.result; if (!cursor) { @@ -648,10 +707,9 @@ function keys(callback) { 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); - + return createTransaction(self._dbInfo, 'readonly'); + }).then(function(transaction) { + var store = transaction.objectStore(self._dbInfo.storeName); var req = store.openCursor(); var keys = []; From bcf62c531a52b2858c3938f98e7d2706e20fbdff Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sat, 6 May 2017 23:49:35 +0300 Subject: [PATCH 02/10] test(api): test IDBD reconnect after InvalidStateError --- examples/indexeddb-invalidstate.html | 38 ++++++++++++++++++++++++++++ test/test.api.js | 32 +++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 examples/indexeddb-invalidstate.html diff --git a/examples/indexeddb-invalidstate.html b/examples/indexeddb-invalidstate.html new file mode 100644 index 0000000..578bf86 --- /dev/null +++ b/examples/indexeddb-invalidstate.html @@ -0,0 +1,38 @@ + + + + + Simple localForage example + + + + + + diff --git a/test/test.api.js b/test/test.api.js index 71b897e..63a07cc 100644 --- a/test/test.api.js +++ b/test/test.api.js @@ -264,6 +264,38 @@ DRIVERS.forEach(function(driverName) { localforage._dbInfo.db.transaction = transaction; }); }); + + describe('recover (reconnect) from IDBDatabase InvalidStateError', function() { + + beforeEach(function(done) { + localforage.setItem('key', 'value1').then(function() { + localforage._dbInfo.db.close(); + done(); + }, function(error) { + done(error || 'error'); + }); + }); + + it('retrieves an item from the storage', function(done) { + localforage.getItem('key').then(function(value) { + expect(value).to.be('value1'); + done(); + }, function(error) { + done(error || 'error'); + }); + }); + + it('stores and retrieves an item from the storage', function(done) { + localforage.setItem('key', 'value2').then(function() { + return localforage.getItem('key'); + }).then(function(value) { + expect(value).to.be('value2'); + done(); + }, function(error) { + done(error || 'error'); + }); + }); + }); } if (driverName === localforage.WEBSQL) { From 7f75f07f8f93ae41e9a78a154719e47f1f2a2334 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sat, 6 May 2017 15:16:19 +0300 Subject: [PATCH 03/10] fix(indexeddb): check db existence before closing --- src/drivers/indexeddb.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/drivers/indexeddb.js b/src/drivers/indexeddb.js index b27d276..c2b4201 100644 --- a/src/drivers/indexeddb.js +++ b/src/drivers/indexeddb.js @@ -285,8 +285,10 @@ function _tryReconnect(dbInfo) { var forages = dbContext.forages; for (var i = 0; i < forages.length; i++) { - forages[i]._dbInfo.db.close(); - forages[i]._dbInfo.db = null; + if (forages[i]._dbInfo.db) { + forages[i]._dbInfo.db.close(); + forages[i]._dbInfo.db = null; + } } return _getConnection(dbInfo, false).then(function(db) { From 1d17b300ad0a9fd0b7c6c831a1b8d74a92baead7 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sun, 7 May 2017 10:55:52 +0300 Subject: [PATCH 04/10] fix(indexeddb): try re-establish DB connection using callbacks --- src/drivers/indexeddb.js | 409 ++++++++++++++++++++++----------------- 1 file changed, 236 insertions(+), 173 deletions(-) diff --git a/src/drivers/indexeddb.js b/src/drivers/indexeddb.js index c2b4201..a06ecfc 100644 --- a/src/drivers/indexeddb.js +++ b/src/drivers/indexeddb.js @@ -301,23 +301,27 @@ function _tryReconnect(dbInfo) { }); } -function createTransaction(dbInfo, mode) { + +// FF doesn't like Promises (micro-tasks) and IDDB store operations, +// so we have to do it with callbacks +function createTransaction(dbInfo, mode, callback) { try { var tx = dbInfo.db.transaction(dbInfo.storeName, mode); - return Promise.resolve(tx); + callback(null, tx); } catch (err) { if (err.name === 'InvalidStateError') { return _tryReconnect(dbInfo).then(function() { var tx = dbInfo.db.transaction(dbInfo.storeName, mode); - return Promise.resolve(tx); + callback(null, tx); }); } - return Promise.reject(err); + callback(err); } } + // Open the IndexedDB database (automatically creates one if one didn't // previously exist), using any options set in the config. function _initStorage(options) { @@ -424,25 +428,33 @@ function getItem(key, callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readonly'); - }).then(function(transaction) { - var store = transaction.objectStore(self._dbInfo.storeName); - var req = store.get(key); - - req.onsuccess = function() { - var value = req.result; - if (value === undefined) { - value = null; + createTransaction(self._dbInfo, 'readonly', function(err, transaction) { + if (err) { + return reject(err); } - if (_isEncodedBlob(value)) { - value = _decodeBlob(value); - } - resolve(value); - }; - req.onerror = function() { - reject(req.error); - }; + try { + var store = transaction.objectStore(self._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 (e) { + reject(e); + } + }); }).catch(reject); }); @@ -456,36 +468,44 @@ function iterate(iterator, callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readonly'); - }).then(function(transaction) { - var store = transaction.objectStore(self._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(); + createTransaction(self._dbInfo, 'readonly', function(err, transaction) { + if (err) { + return reject(err); } - }; - req.onerror = function() { - reject(req.error); - }; + try { + var store = transaction.objectStore(self._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 (e) { + reject(e); + } + }); }).catch(reject); }); @@ -518,40 +538,44 @@ function setItem(key, value, callback) { } return value; }).then(function(value) { - return createTransaction(self._dbInfo, 'readwrite').then(function(transaction) { - return [transaction, value]; - }); - }).then(function(txAndValue) { - var transaction = txAndValue[0]; - var value = txAndValue[1]; - var store = transaction.objectStore(self._dbInfo.storeName); - var req = store.put(value, key); - - // 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; + createTransaction(self._dbInfo, 'readwrite', function(err, transaction) { + if (err) { + return reject(err); } - resolve(value); - }; - transaction.onabort = transaction.onerror = function() { - var err = req.error ? req.error : req.transaction.error; - reject(err); - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.put(value, key); + + // 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); + }; + } catch (e) { + reject(e); + } + }); }).catch(reject); }); @@ -571,30 +595,37 @@ function removeItem(key, callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readwrite'); - }).then(function(transaction) { - var store = transaction.objectStore(self._dbInfo.storeName); + createTransaction(self._dbInfo, 'readwrite', function(err, transaction) { + if (err) { + return reject(err); + } - // 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(); - }; + try { + var store = transaction.objectStore(self._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); - }; + 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); - }; + // 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 (e) { + reject(e); + } + }); }).catch(reject); }); @@ -607,19 +638,27 @@ function clear(callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readwrite'); - }).then(function(transaction) { - var store = transaction.objectStore(self._dbInfo.storeName); - var req = store.clear(); + createTransaction(self._dbInfo, 'readwrite', function(err, transaction) { + if (err) { + return reject(err); + } - transaction.oncomplete = function() { - resolve(); - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.clear(); - transaction.onabort = transaction.onerror = function() { - var err = req.error ? req.error : req.transaction.error; - reject(err); - }; + transaction.oncomplete = function() { + resolve(); + }; + + transaction.onabort = transaction.onerror = function() { + var err = req.error ? req.error : req.transaction.error; + reject(err); + }; + } catch (e) { + reject(e); + } + }); }).catch(reject); }); @@ -632,18 +671,26 @@ function length(callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readonly'); - }).then(function(transaction) { - var store = transaction.objectStore(self._dbInfo.storeName); - var req = store.count(); + createTransaction(self._dbInfo, 'readonly', function(err, transaction) { + if (err) { + return reject(err); + } - req.onsuccess = function() { - resolve(req.result); - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.count(); - req.onerror = function() { - reject(req.error); - }; + req.onsuccess = function() { + resolve(req.result); + }; + + req.onerror = function() { + reject(req.error); + }; + } catch (e) { + reject(e); + } + }); }).catch(reject); }); @@ -662,41 +709,49 @@ function key(n, callback) { } self.ready().then(function() { - return createTransaction(self._dbInfo, 'readonly'); - }).then(function(transaction) { - var store = transaction.objectStore(self._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; + createTransaction(self._dbInfo, 'readonly', function(err, transaction) { + if (err) { + return reject(err); } - 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); - } - } - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var advanced = false; + var req = store.openCursor(); - req.onerror = function() { - reject(req.error); - }; + 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 (e) { + reject(e); + } + }); }).catch(reject); }); @@ -709,27 +764,35 @@ function keys(callback) { var promise = new Promise(function(resolve, reject) { self.ready().then(function() { - return createTransaction(self._dbInfo, 'readonly'); - }).then(function(transaction) { - var store = transaction.objectStore(self._dbInfo.storeName); - var req = store.openCursor(); - var keys = []; - - req.onsuccess = function() { - var cursor = req.result; - - if (!cursor) { - resolve(keys); - return; + createTransaction(self._dbInfo, 'readonly', function(err, transaction) { + if (err) { + return reject(err); } - keys.push(cursor.key); - cursor.continue(); - }; + try { + var store = transaction.objectStore(self._dbInfo.storeName); + var req = store.openCursor(); + var keys = []; - req.onerror = function() { - reject(req.error); - }; + 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 (e) { + reject(e); + } + }); }).catch(reject); }); From 85bc1d2c5f9ef2ef578cf5838b27eccf43b79d04 Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Sun, 14 May 2017 12:43:17 +0300 Subject: [PATCH 05/10] style(examples/indexeddb-invalidstate): remove some comments --- examples/indexeddb-invalidstate.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/indexeddb-invalidstate.html b/examples/indexeddb-invalidstate.html index 578bf86..cb49cc7 100644 --- a/examples/indexeddb-invalidstate.html +++ b/examples/indexeddb-invalidstate.html @@ -7,18 +7,17 @@