Merge pull request #694 from thgreasi/idb-reconnect-tests-rebase

feat(indexeddb): try reconnect to the DB after InvalidStateError
This commit is contained in:
Thodoris Greasidis 2017-05-14 15:47:35 +03:00 committed by GitHub
commit 5e93acf5a9
7 changed files with 1000 additions and 491 deletions

454
dist/localforage.js vendored
View File

@ -470,6 +470,10 @@ var supportsBlobs;
var dbContexts;
var toString = Object.prototype.toString;
// Transaction Modes
var READ_ONLY = 'readonly';
var READ_WRITE = 'readwrite';
// 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.
@ -502,7 +506,7 @@ function _binStringToArrayBuffer(bin) {
//
function _checkBlobSupportWithoutCaching(idb) {
return new Promise$1(function (resolve) {
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, 'readwrite');
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE);
var blob = createBlob(['']);
txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key');
@ -572,6 +576,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$1(function (resolve, reject) {
@ -714,6 +731,51 @@ 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++) {
if (forages[i]._dbInfo.db) {
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;
});
}
// 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);
callback(null, tx);
} catch (err) {
if (!dbInfo.db || err.name === 'InvalidStateError') {
return _tryReconnect(dbInfo).then(function () {
var tx = dbInfo.db.transaction(dbInfo.storeName, mode);
callback(null, tx);
});
}
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) {
@ -820,24 +882,33 @@ function getItem(key, callback) {
var promise = new Promise$1(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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -851,35 +922,46 @@ function iterate(iterator, callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_ONLY, 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++);
// when the iterator callback retuns any
// (non-`undefined`) value, then we stop
// the iteration immediately
if (result !== void 0) {
resolve(result);
} else {
cursor["continue"]();
}
} else {
resolve();
}
};
req.onerror = function () {
reject(req.error);
};
} catch (e) {
reject(e);
}
});
})["catch"](reject);
});
@ -911,35 +993,44 @@ function setItem(key, value, callback) {
}
return value;
}).then(function (value) {
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
var store = transaction.objectStore(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, READ_WRITE, 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);
});
@ -958,30 +1049,37 @@ function removeItem(key, callback) {
var promise = new Promise$1(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);
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -994,19 +1092,27 @@ function clear(callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -1019,17 +1125,26 @@ function length(callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -1048,40 +1163,49 @@ function key(n, callback) {
}
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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -1094,27 +1218,35 @@ function keys(callback) {
var promise = new Promise$1(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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});

File diff suppressed because one or more lines are too long

View File

@ -134,6 +134,10 @@ var supportsBlobs;
var dbContexts;
var toString = Object.prototype.toString;
// Transaction Modes
var READ_ONLY = 'readonly';
var READ_WRITE = 'readwrite';
// 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.
@ -166,7 +170,7 @@ function _binStringToArrayBuffer(bin) {
//
function _checkBlobSupportWithoutCaching(idb) {
return new Promise$1(function (resolve) {
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, 'readwrite');
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE);
var blob = createBlob(['']);
txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key');
@ -236,6 +240,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$1(function (resolve, reject) {
@ -378,6 +395,51 @@ 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++) {
if (forages[i]._dbInfo.db) {
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;
});
}
// 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);
callback(null, tx);
} catch (err) {
if (!dbInfo.db || err.name === 'InvalidStateError') {
return _tryReconnect(dbInfo).then(function () {
var tx = dbInfo.db.transaction(dbInfo.storeName, mode);
callback(null, tx);
});
}
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) {
@ -484,24 +546,33 @@ function getItem(key, callback) {
var promise = new Promise$1(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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -515,35 +586,46 @@ function iterate(iterator, callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_ONLY, 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++);
// when the iterator callback retuns any
// (non-`undefined`) value, then we stop
// the iteration immediately
if (result !== void 0) {
resolve(result);
} else {
cursor["continue"]();
}
} else {
resolve();
}
};
req.onerror = function () {
reject(req.error);
};
} catch (e) {
reject(e);
}
});
})["catch"](reject);
});
@ -575,35 +657,44 @@ function setItem(key, value, callback) {
}
return value;
}).then(function (value) {
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
var store = transaction.objectStore(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, READ_WRITE, 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);
});
@ -622,30 +713,37 @@ function removeItem(key, callback) {
var promise = new Promise$1(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);
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -658,19 +756,27 @@ function clear(callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -683,17 +789,26 @@ function length(callback) {
var promise = new Promise$1(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();
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -712,40 +827,49 @@ function key(n, callback) {
}
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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -758,27 +882,35 @@ function keys(callback) {
var promise = new Promise$1(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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<meta charset="utf8" />
<title>Simple localForage example</title>
</head>
<body>
<script src="../dist/localforage.js"></script>
<script>
// Forcing IndexedDB here.
localforage.setDriver(localforage.INDEXEDDB).then(function() {
var key = 'STORE_KEY';
var value = new Uint8Array(8);
value[0] = 65
var UNKNOWN_KEY = 'unknown_key';
localforage.setItem(key, value, function() {
console.log('Saved: ' + value);
// causes InvalidState erros
localforage._dbInfo.db.close();
localforage.getItem(key).then(function(readValue) {
console.log('Read: ', readValue);
}).catch(function(err) {
console.error('Read: ', err);
});
// Since this key hasn't been set yet, we'll get a null value
localforage.getItem(UNKNOWN_KEY).then(function(err, readValue) {
console.log('Result of reading ' + UNKNOWN_KEY, readValue);
});
});
});
</script>
</body>
</html>

View File

@ -12,6 +12,10 @@ var supportsBlobs;
var dbContexts;
var toString = Object.prototype.toString;
// Transaction Modes
var READ_ONLY = 'readonly';
var READ_WRITE = 'readwrite';
// 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.
@ -44,7 +48,7 @@ function _binStringToArrayBuffer(bin) {
//
function _checkBlobSupportWithoutCaching(idb) {
return new Promise(function(resolve) {
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, 'readwrite');
var txn = idb.transaction(DETECT_BLOB_SUPPORT_STORE, READ_WRITE);
var blob = createBlob(['']);
txn.objectStore(DETECT_BLOB_SUPPORT_STORE).put(blob, 'key');
@ -115,6 +119,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 +279,54 @@ 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++) {
if (forages[i]._dbInfo.db) {
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;
});
}
// 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);
callback(null, tx);
} catch (err) {
if (!dbInfo.db ||
err.name === 'InvalidStateError') {
return _tryReconnect(dbInfo).then(function() {
var tx = dbInfo.db.transaction(dbInfo.storeName, mode);
callback(null, tx);
});
}
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) {
@ -355,6 +420,7 @@ function _initStorage(options) {
});
}
function getItem(key, callback) {
var self = this;
@ -367,25 +433,33 @@ 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);
var req = store.get(key);
req.onsuccess = function() {
var value = req.result;
if (value === undefined) {
value = null;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -399,37 +473,47 @@ 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);
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, READ_ONLY, 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++);
// when the iterator callback retuns any
// (non-`undefined`) value, then we stop
// the iteration immediately
if (result !== void(0)) {
resolve(result);
} else {
cursor.continue();
}
} else {
resolve();
}
};
req.onerror = function() {
reject(req.error);
};
} catch (e) {
reject(e);
}
});
}).catch(reject);
});
@ -462,35 +546,44 @@ function setItem(key, value, callback) {
}
return value;
}).then(function(value) {
var transaction = dbInfo.db.transaction(dbInfo.storeName, 'readwrite');
var store = transaction.objectStore(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, READ_WRITE, 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);
});
@ -510,30 +603,37 @@ 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);
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -546,19 +646,27 @@ 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);
var req = store.clear();
createTransaction(self._dbInfo, READ_WRITE, 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);
});
@ -571,18 +679,26 @@ 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);
var req = store.count();
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -601,41 +717,49 @@ function key(n, callback) {
}
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;
createTransaction(self._dbInfo, READ_ONLY, 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);
});
@ -648,28 +772,35 @@ 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);
var req = store.openCursor();
var keys = [];
req.onsuccess = function() {
var cursor = req.result;
if (!cursor) {
resolve(keys);
return;
createTransaction(self._dbInfo, READ_ONLY, 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);
});

View File

@ -264,6 +264,83 @@ DRIVERS.forEach(function(driverName) {
localforage._dbInfo.db.transaction = transaction;
});
});
describe('recover (reconnect) from IDBDatabase InvalidStateError', function() {
beforeEach(function(done) {
Promise.all([
localforage.setItem('key', 'value1'),
localforage.setItem('key1', 'value1'),
localforage.setItem('key2', 'value2'),
localforage.setItem('key3', 'value3')
]).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('retrieves more than one items from the storage', function(done) {
Promise.all([
localforage.getItem('key1'),
localforage.getItem('key2'),
localforage.getItem('key3')
]).then(function(values) {
expect(values).to.eql([
'value1',
'value2',
'value3'
]);
done();
}, function(error) {
done(error || 'error');
});
});
it('stores and retrieves an item from the storage', function(done) {
localforage.setItem('key', 'value1b').then(function() {
return localforage.getItem('key');
}).then(function(value) {
expect(value).to.be('value1b');
done();
}, function(error) {
done(error || 'error');
});
});
it('stores and retrieves more than one items from the storage', function(done) {
Promise.all([
localforage.setItem('key1', 'value1b'),
localforage.setItem('key2', 'value2b'),
localforage.setItem('key3', 'value3b')
]).then(function() {
return Promise.all([
localforage.getItem('key1'),
localforage.getItem('key2'),
localforage.getItem('key3')
]);
}).then(function(values) {
expect(values).to.eql([
'value1b',
'value2b',
'value3b'
]);
done();
}, function(error) {
done(error || 'error');
});
});
});
}
if (driverName === localforage.WEBSQL) {