Commit e49810ca authored by Romain Courteaud's avatar Romain Courteaud

WIP indexedDB: major rewrite

Open database is now wrapped into a promise which ensure that the database will be closed as soon as the promise is resolved.
This will allow to open multiple transactions if needed.

Transaction creation is now manage by a promise, which will abort the transaction if cancel is called.

The transaction will also be aborted if an error occurs during its callback function.

Check the transaction error event to detect some browser internal errors (like QuotaExceededError).
Previously, such error was silently ignored, and the jIO client was not aware that the data were not correctly written.

All deletions are now correctly checked, and the method will wait for all deletion to be successfull before returning.

Improve putAttachment algorithm to make it update the previous value and delete only the useless blob chunks.

Reduce the usage of openCursor to not fetch the table content if not needed (like if the index key is enough for example).
parent 8b900961
...@@ -96,8 +96,7 @@ ...@@ -96,8 +96,7 @@
store.createIndex("_id", "_id", {unique: false}); store.createIndex("_id", "_id", {unique: false});
} }
function openIndexedDB(jio_storage) { function waitForOpenIndexedDB(db_name, callback) {
var db_name = jio_storage._database_name;
function resolver(resolve, reject) { function resolver(resolve, reject) {
// Open DB // // Open DB //
var request = indexedDB.open(db_name); var request = indexedDB.open(db_name);
...@@ -139,47 +138,86 @@ ...@@ -139,47 +138,86 @@
}; };
request.onsuccess = function () { request.onsuccess = function () {
resolve(request.result);
};
}
// XXX Canceller???
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
return new RSVP.Promise(resolver); return callback(request.result);
})
.push(function (result) {
request.result.close();
resolve(result);
}, function (error) {
request.result.close();
reject(error);
}); });
};
}
return new RSVP.Promise(resolver);
} }
function openTransaction(db, stores, flag, autoclosedb) { function waitForTransaction(db, stores, flag, callback) {
var tx = db.transaction(stores, flag); var tx = db.transaction(stores, flag);
if (autoclosedb !== false) { function canceller() {
try {
tx.abort();
} catch (unused) {
// Transaction already finished
return;
}
}
function resolver(resolve, reject) {
var result;
try {
result = callback(tx);
} catch (error) {
reject(error);
}
tx.oncomplete = function () { tx.oncomplete = function () {
db.close(); return new RSVP.Queue()
.push(function () {
return result;
})
.push(resolve, function (error) {
canceller();
reject(error);
});
}; };
} tx.onerror = function (error) {
tx.onabort = function () { canceller();
db.close(); reject(error);
};
tx.onabort = function (evt) {
reject(evt.target);
}; };
return tx; return tx;
} }
return new RSVP.Promise(resolver, canceller);
}
function handleCursor(request, callback, resolve, reject) { function waitForIDBRequest(request) {
request.onerror = function (error) { return new RSVP.Promise(function (resolve, reject) {
if (request.transaction) { request.onerror = reject;
request.transaction.abort(); request.onsuccess = resolve;
});
} }
reject(error);
};
function waitForAllSynchronousCursor(request, callback) {
var force_cancellation = false;
function canceller() {
force_cancellation = true;
}
function resolver(resolve, reject) {
request.onerror = reject;
request.onsuccess = function (evt) { request.onsuccess = function (evt) {
var cursor = evt.target.result; var cursor = evt.target.result;
if (cursor) { if (cursor && !force_cancellation) {
// XXX Wait for result
try { try {
callback(cursor); callback(cursor);
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
// continue to next iteration // continue to next iteration
cursor["continue"](); cursor["continue"]();
} else { } else {
...@@ -187,9 +225,12 @@ ...@@ -187,9 +225,12 @@
} }
}; };
} }
return new RSVP.Promise(resolver, canceller);
}
IndexedDBStorage.prototype.buildQuery = function (options) { IndexedDBStorage.prototype.buildQuery = function (options) {
var result_list = []; var result_list = [],
context = this;
function pushIncludedMetadata(cursor) { function pushIncludedMetadata(cursor) {
result_list.push({ result_list.push({
...@@ -205,17 +246,23 @@ ...@@ -205,17 +246,23 @@
"value": {} "value": {}
}); });
} }
return openIndexedDB(this)
.push(function (db) { return new RSVP.Queue()
return new RSVP.Promise(function (resolve, reject) { .push(function () {
var tx = openTransaction(db, ["metadata"], "readonly"); return waitForOpenIndexedDB(context._database_name, function (db) {
return waitForTransaction(db, ["metadata"], "readonly",
function (tx) {
if (options.include_docs === true) { if (options.include_docs === true) {
handleCursor(tx.objectStore("metadata").index("_id").openCursor(), return waitForAllSynchronousCursor(
pushIncludedMetadata, resolve, reject); tx.objectStore("metadata").index("_id").openCursor(),
} else { pushIncludedMetadata
handleCursor(tx.objectStore("metadata").index("_id") );
.openKeyCursor(), pushMetadata, resolve, reject);
} }
return waitForAllSynchronousCursor(
tx.objectStore("metadata").index("_id").openKeyCursor(),
pushMetadata
);
});
}); });
}) })
.push(function () { .push(function () {
...@@ -223,263 +270,313 @@ ...@@ -223,263 +270,313 @@
}); });
}; };
function handleGet(store, id, resolve, reject) {
var request = store.get(id);
request.onerror = reject;
request.onsuccess = function () {
if (request.result) {
resolve(request.result);
} else {
reject(new jIO.util.jIOError(
"IndexedDB: cannot find object '" + id + "' in the '" +
store.name + "' store",
404
));
}
};
}
IndexedDBStorage.prototype.get = function (id) { IndexedDBStorage.prototype.get = function (id) {
return openIndexedDB(this) var context = this;
.push(function (db) { return new RSVP.Queue()
return new RSVP.Promise(function (resolve, reject) { .push(function () {
var transaction = openTransaction(db, ["metadata"], "readonly"); return waitForOpenIndexedDB(context._database_name, function (db) {
handleGet( return waitForTransaction(db, ["metadata"], "readonly",
transaction.objectStore("metadata"), function (tx) {
id, return waitForIDBRequest(tx.objectStore("metadata").get(id));
resolve, });
reject
);
}); });
}) })
.push(function (result) { .push(function (evt) {
return result.doc; if (evt.target.result) {
return evt.target.result.doc;
}
throw new jIO.util.jIOError(
"IndexedDB: cannot find object '" + id + "' in the 'metadata' store",
404
);
}); });
}; };
IndexedDBStorage.prototype.allAttachments = function (id) { IndexedDBStorage.prototype.allAttachments = function (id) {
var attachment_dict = {}; var attachment_dict = {},
context = this;
function addEntry(cursor) { function addEntry(cursor) {
attachment_dict[cursor.value._attachment] = {}; attachment_dict[cursor.primaryKey.slice(cursor.key.length + 1)] = {};
} }
return openIndexedDB(this) return new RSVP.Queue()
.push(function (db) { .push(function () {
return new RSVP.Promise(function (resolve, reject) { return waitForOpenIndexedDB(context._database_name, function (db) {
var transaction = openTransaction(db, ["metadata", "attachment"], return waitForTransaction(db, ["metadata", "attachment"], "readonly",
"readonly"); function (tx) {
function getAttachments() { return RSVP.all([
handleCursor( waitForIDBRequest(tx.objectStore("metadata").get(id)),
transaction.objectStore("attachment").index("_id") waitForAllSynchronousCursor(
.openCursor(IDBKeyRange.only(id)), tx.objectStore("attachment").index("_id")
addEntry, .openKeyCursor(IDBKeyRange.only(id)),
resolve, addEntry
reject )
); ]);
} });
handleGet(
transaction.objectStore("metadata"),
id,
getAttachments,
reject
);
}); });
}) })
.push(function () { .push(function (result_list) {
var evt = result_list[0];
if (!evt.target.result) {
throw new jIO.util.jIOError(
"IndexedDB: cannot find object '" + id +
"' in the 'metadata' store",
404
);
}
return attachment_dict; return attachment_dict;
}); });
}; };
function handleRequest(request, resolve, reject) {
request.onerror = reject;
request.onsuccess = function () {
resolve(request.result);
};
}
IndexedDBStorage.prototype.put = function (id, metadata) { IndexedDBStorage.prototype.put = function (id, metadata) {
return openIndexedDB(this) return waitForOpenIndexedDB(this._database_name, function (db) {
.push(function (db) { return waitForTransaction(db, ["metadata"], "readwrite",
return new RSVP.Promise(function (resolve, reject) { function (tx) {
var transaction = openTransaction(db, ["metadata"], "readwrite"); return waitForIDBRequest(tx.objectStore("metadata").put({
handleRequest(
transaction.objectStore("metadata").put({
"_id": id, "_id": id,
"doc": metadata "doc": metadata
}), }));
resolve,
reject
);
}); });
}); });
}; };
function deleteEntry(cursor) {
cursor["delete"]();
}
IndexedDBStorage.prototype.remove = function (id) { IndexedDBStorage.prototype.remove = function (id) {
var resolved_amount = 0; return waitForOpenIndexedDB(this._database_name, function (db) {
return openIndexedDB(this) return waitForTransaction(db, ["metadata", "attachment", "blob"],
.push(function (db) { "readwrite", function (tx) {
return new RSVP.Promise(function (resolve, reject) {
function resolver() { var promise_list = [],
if (resolved_amount < 2) { metadata_store = tx.objectStore("metadata"),
resolved_amount += 1; attachment_store = tx.objectStore("attachment"),
} else { blob_store = tx.objectStore("blob");
resolve();
} function deleteAttachment(cursor) {
} promise_list.push(
var transaction = openTransaction(db, ["metadata", "attachment", waitForIDBRequest(attachment_store.delete(cursor.primaryKey))
"blob"], "readwrite");
handleRequest(
transaction.objectStore("metadata")["delete"](id),
resolver,
reject
);
// XXX Why not possible to delete with KeyCursor?
handleCursor(transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)),
deleteEntry,
resolver,
reject
); );
handleCursor(transaction.objectStore("blob").index("_id") }
.openCursor(IDBKeyRange.only(id)), function deleteBlob(cursor) {
deleteEntry, promise_list.push(
resolver, waitForIDBRequest(blob_store.delete(cursor.primaryKey))
reject
); );
}
return RSVP.all([
waitForIDBRequest(metadata_store.delete(id)),
waitForAllSynchronousCursor(
attachment_store.index("_id")
.openKeyCursor(IDBKeyRange.only(id)),
deleteAttachment
),
waitForAllSynchronousCursor(
blob_store.index("_id")
.openKeyCursor(IDBKeyRange.only(id)),
deleteBlob
),
])
.then(function () {
return RSVP.all(promise_list);
});
}); });
}); });
}; };
IndexedDBStorage.prototype.getAttachment = function (id, name, options) { IndexedDBStorage.prototype.getAttachment = function (id, name, options) {
var transaction,
type,
start,
end;
if (options === undefined) { if (options === undefined) {
options = {}; options = {};
} }
return openIndexedDB(this) var db_name = this._database_name,
.push(function (db) { start,
return new RSVP.Promise(function (resolve, reject) { end,
transaction = openTransaction( array_buffer_list = [];
db,
["attachment", "blob"],
"readonly"
);
function getBlob(attachment) {
var total_length = attachment.info.length,
result_list = [],
store = transaction.objectStore("blob"),
start_index,
end_index;
type = attachment.info.content_type;
start = options.start || 0; start = options.start || 0;
end = options.end || total_length; end = options.end;
if (end > total_length) {
end = total_length; // Stream the blob content
} if ((start !== 0) || (end !== undefined)) {
if (start < 0 || end < 0) {
if (start < 0 || ((end !== undefined) && (end < 0))) {
throw new jIO.util.jIOError( throw new jIO.util.jIOError(
"_start and _end must be positive", "_start and _end must be positive",
400 400
); );
} }
if (start > end) { if ((end !== undefined) && (start > end)) {
throw new jIO.util.jIOError("_start is greater than _end", throw new jIO.util.jIOError("_start is greater than _end",
400); 400);
} }
return new RSVP.Queue()
.push(function () {
return waitForOpenIndexedDB(db_name, function (db) {
return waitForTransaction(db, ["blob"], "readonly",
function (tx) {
var key_path = buildKeyPath([id, name]),
blob_store = tx.objectStore("blob"),
start_index,
end_index,
promise_list = [];
start_index = Math.floor(start / UNITE); start_index = Math.floor(start / UNITE);
end_index = Math.floor(end / UNITE) - 1; if (end !== undefined) {
end_index = Math.floor(end / UNITE);
if (end % UNITE === 0) { if (end % UNITE === 0) {
end_index -= 1; end_index -= 1;
} }
function resolver(result) {
if (result.blob !== undefined) {
result_list.push(result);
} }
resolve(result_list);
} function getBlobKey(cursor) {
function getPart(i) { var index = parseInt(
return function (result) { cursor.primaryKey.slice(key_path.length + 1),
if (result) { 10
result_list.push(result); ),
i;
if ((start !== 0) && (index < start_index)) {
// No need to fetch blobs at the start
return;
} }
i += 1; if ((end !== undefined) && (index > end_index)) {
handleGet(store, // No need to fetch blobs at the end
buildKeyPath([id, name, i]), return;
(i <= end_index) ? getPart(i) : resolver,
reject
);
};
} }
getPart(start_index - 1)();
i = index - start_index;
// Extend array size
while (i > promise_list.length) {
promise_list.push(null);
i -= 1;
} }
// XXX Should raise if key is not good // Sort the blob by their index
handleGet(transaction.objectStore("attachment"), promise_list.splice(
buildKeyPath([id, name]), index - start_index,
getBlob, 0,
reject waitForIDBRequest(blob_store.get(cursor.primaryKey))
); );
}
// Get all blob keys to check if they must be fetched
return waitForAllSynchronousCursor(
blob_store.index("_id_attachment")
.openKeyCursor(IDBKeyRange.only([id, name])),
getBlobKey
)
.then(function () {
return RSVP.all(promise_list);
});
});
}); });
}) })
.push(function (result_list) { .push(function (result_list) {
var array_buffer_list = [], // No need to keep the IDB open
blob, var blob,
i,
index, index,
len = result_list.length; i;
for (i = 0; i < len; i += 1) {
array_buffer_list.push(result_list[i].blob); for (i = 0; i < result_list.length; i += 1) {
} array_buffer_list.push(result_list[i].target.result.blob);
if ((options.start === undefined) && (options.end === undefined)) {
return new Blob(array_buffer_list, {type: type});
} }
blob = new Blob(array_buffer_list,
{type: "application/octet-stream"});
index = Math.floor(start / UNITE) * UNITE; index = Math.floor(start / UNITE) * UNITE;
blob = new Blob(array_buffer_list, {type: "application/octet-stream"}); if (end === undefined) {
return blob.slice(start - index, end - index, end = blob.length;
} else {
end = end - index;
}
return blob.slice(start - index, end,
"application/octet-stream"); "application/octet-stream");
}); });
}; }
// Request the full blob
return new RSVP.Queue()
.push(function () {
return waitForOpenIndexedDB(db_name, function (db) {
return waitForTransaction(db, ["attachment", "blob"], "readonly",
function (tx) {
var key_path = buildKeyPath([id, name]),
attachment_store = tx.objectStore("attachment"),
blob_store = tx.objectStore("blob");
function getBlob(cursor) {
var index = parseInt(
cursor.primaryKey.slice(key_path.length + 1),
10
),
i = index;
// Extend array size
while (i > array_buffer_list.length) {
array_buffer_list.push(null);
i -= 1;
}
// Sort the blob by their index
array_buffer_list.splice(
index,
0,
cursor.value.blob
);
}
function removeAttachment(transaction, id, name, resolve, reject) { return RSVP.all([
// XXX How to get the right attachment // Get the attachment info (mime type)
function deleteContent() { waitForIDBRequest(attachment_store.get(
handleCursor( key_path
transaction.objectStore("blob").index("_id_attachment") )),
// Get all needed blobs
waitForAllSynchronousCursor(
blob_store.index("_id_attachment")
.openCursor(IDBKeyRange.only([id, name])), .openCursor(IDBKeyRange.only([id, name])),
deleteEntry, getBlob
resolve, )
reject ]);
});
});
})
.push(function (result_list) {
// No need to keep the IDB open
var blob,
attachment = result_list[0].target.result;
// Should raise if key is not good
if (!attachment) {
throw new jIO.util.jIOError(
"IndexedDB: cannot find object '" +
buildKeyPath([id, name]) +
"' in the 'attachment' store",
404
); );
} }
handleRequest(
transaction.objectStore("attachment")["delete"]( blob = new Blob(array_buffer_list,
buildKeyPath([id, name]) {type: attachment.info.content_type});
), if (blob.length !== attachment.info.total_length) {
deleteContent, throw new jIO.util.jIOError(
reject "IndexedDB: attachment '" +
buildKeyPath([id, name]) +
"' in the 'attachment' store is broken",
500
); );
} }
return blob;
});
};
IndexedDBStorage.prototype.putAttachment = function (id, name, blob) { IndexedDBStorage.prototype.putAttachment = function (id, name, blob) {
var blob_part = [], var db_name = this._database_name;
transaction, return new RSVP.Queue()
db; .push(function () {
return openIndexedDB(this)
.push(function (database) {
db = database;
// Split the blob first // Split the blob first
return jIO.util.readBlobAsArrayBuffer(blob); return jIO.util.readBlobAsArrayBuffer(blob);
}) })
.push(function (event) { .push(function (event) {
var array_buffer = event.target.result, var array_buffer = event.target.result,
blob_part = [],
total_size = blob.size, total_size = blob.size,
handled_size = 0; handled_size = 0;
...@@ -489,55 +586,100 @@ ...@@ -489,55 +586,100 @@
handled_size += UNITE; handled_size += UNITE;
} }
// Remove previous attachment return waitForOpenIndexedDB(db_name, function (db) {
transaction = openTransaction(db, ["attachment", "blob"], "readwrite"); return waitForTransaction(db, ["attachment", "blob"], "readwrite",
return new RSVP.Promise(function (resolve, reject) { function (tx) {
function write() { var blob_store,
var len = blob_part.length - 1, promise_list,
attachment_store = transaction.objectStore("attachment"), delete_promise_list = [],
blob_store = transaction.objectStore("blob"); key_path = buildKeyPath([id, name]),
function putBlobPart(i) { i;
return function () { // First write the attachment info on top of previous
i += 1; promise_list = [
handleRequest( waitForIDBRequest(tx.objectStore("attachment").put({
blob_store.put({ "_key_path": key_path,
"_id": id,
"_attachment": name,
"info": {
"content_type": blob.type,
"length": blob.size
}
}))
];
// Then, write all blob parts on top of previous
blob_store = tx.objectStore("blob");
for (i = 0; i < blob_part.length; i += 1) {
promise_list.push(
waitForIDBRequest(blob_store.put({
"_key_path": buildKeyPath([id, name, i]), "_key_path": buildKeyPath([id, name, i]),
"_id" : id, "_id" : id,
"_attachment" : name, "_attachment" : name,
"_part" : i, "_part" : i,
"blob": blob_part[i] "blob": blob_part[i]
}), }))
(i < len) ? putBlobPart(i) : resolve, );
reject }
function deleteEntry(cursor) {
var index = parseInt(
cursor.primaryKey.slice(key_path.length + 1),
10
);
if (index >= blob_part.length) {
delete_promise_list.push(
waitForIDBRequest(blob_store.delete(cursor.primaryKey))
); );
};
} }
handleRequest(
attachment_store.put({
"_key_path": buildKeyPath([id, name]),
"_id": id,
"_attachment": name,
"info": {
"content_type": blob.type,
"length": blob.size
} }
}),
putBlobPart(-1), // Finally, remove all remaining blobs
reject promise_list.push(
waitForAllSynchronousCursor(
blob_store.index("_id_attachment")
.openKeyCursor(IDBKeyRange.only([id, name])),
deleteEntry
)
); );
return RSVP.all(promise_list)
.then(function () {
if (delete_promise_list.length) {
return RSVP.all(delete_promise_list);
} }
removeAttachment(transaction, id, name, write, reject); });
});
}); });
}); });
}; };
IndexedDBStorage.prototype.removeAttachment = function (id, name) { IndexedDBStorage.prototype.removeAttachment = function (id, name) {
return openIndexedDB(this) return waitForOpenIndexedDB(this._database_name, function (db) {
.push(function (db) { return waitForTransaction(db, ["attachment", "blob"], "readwrite",
var transaction = openTransaction(db, ["attachment", "blob"], function (tx) {
"readwrite"); var promise_list = [],
return new RSVP.Promise(function (resolve, reject) { attachment_store = tx.objectStore("attachment"),
removeAttachment(transaction, id, name, resolve, reject); blob_store = tx.objectStore("blob");
function deleteEntry(cursor) {
promise_list.push(
waitForIDBRequest(blob_store.delete(cursor.primaryKey))
);
}
return RSVP.all([
waitForIDBRequest(
attachment_store.delete(buildKeyPath([id, name]))
),
waitForAllSynchronousCursor(
blob_store.index("_id_attachment")
.openKeyCursor(IDBKeyRange.only([id, name])),
deleteEntry
)
])
.then(function () {
return RSVP.all(promise_list);
});
}); });
}); });
}; };
......
...@@ -39,17 +39,13 @@ ...@@ -39,17 +39,13 @@
} }
function deleteIndexedDB(storage) { function deleteIndexedDB(storage) {
return new RSVP.Queue() return new RSVP.Promise(function resolver(resolve, reject) {
.push(function () {
function resolver(resolve, reject) {
var request = indexedDB.deleteDatabase( var request = indexedDB.deleteDatabase(
storage.__storage._database_name storage.__storage._database_name
); );
request.onerror = reject; request.onerror = reject;
request.onblocked = reject; request.onblocked = reject;
request.onsuccess = resolve; request.onsuccess = resolve;
}
return new RSVP.Promise(resolver);
}); });
} }
...@@ -597,7 +593,7 @@ ...@@ -597,7 +593,7 @@
test("spy indexedDB usage", function () { test("spy indexedDB usage", function () {
var context = this; var context = this;
stop(); stop();
expect(17); expect(18);
deleteIndexedDB(context.jio) deleteIndexedDB(context.jio)
.then(function () { .then(function () {
...@@ -615,6 +611,7 @@ ...@@ -615,6 +611,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
context.spy_key_range = sinon.spy(IDBKeyRange, "only"); context.spy_key_range = sinon.spy(IDBKeyRange, "only");
return context.jio.allAttachments("foo"); return context.jio.allAttachments("foo");
...@@ -654,8 +651,10 @@ ...@@ -654,8 +651,10 @@
deepEqual(context.spy_index.firstCall.args[0], "_id", deepEqual(context.spy_index.firstCall.args[0], "_id",
"index first argument"); "index first argument");
ok(context.spy_cursor.calledOnce, "cursor count " + ok(!context.spy_cursor.called, "cursor count " +
context.spy_cursor.callCount); context.spy_cursor.callCount);
ok(context.spy_key_cursor.calledOnce, "cursor key count " +
context.spy_key_cursor.callCount);
ok(context.spy_key_range.calledOnce, "key range count " + ok(context.spy_key_range.calledOnce, "key range count " +
context.spy_key_range.callCount); context.spy_key_range.callCount);
...@@ -677,6 +676,8 @@ ...@@ -677,6 +676,8 @@
delete context.spy_index; delete context.spy_index;
context.spy_create_index.restore(); context.spy_create_index.restore();
delete context.spy_create_index; delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore(); context.spy_cursor.restore();
delete context.spy_cursor; delete context.spy_cursor;
context.spy_key_range.restore(); context.spy_key_range.restore();
...@@ -891,29 +892,21 @@ ...@@ -891,29 +892,21 @@
context.spy_key_range.callCount); context.spy_key_range.callCount);
}) })
.always(function () {
context.spy_open.restore();
delete context.spy_open;
context.spy_create_store.restore();
delete context.spy_create_store;
context.spy_transaction.restore();
delete context.spy_transaction;
context.spy_store.restore();
delete context.spy_store;
context.spy_put.restore();
delete context.spy_put;
context.spy_index.restore();
delete context.spy_index;
context.spy_create_index.restore();
delete context.spy_create_index;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_key_range.restore();
delete context.spy_key_range;
})
.fail(function (error) { .fail(function (error) {
ok(false, error); ok(false, error);
}) })
.always(function () {
var i,
spy_list = ['spy_open', 'spy_create_store', 'spy_transaction',
'spy_store', 'spy_put', 'spy_index', 'spy_create_index',
'spy_cursor', 'spy_key_range'];
for (i = 0; i < spy_list.length; i += 1) {
if (context.hasOwnProperty(spy_list[i])) {
context[spy_list[i]].restore();
delete context[spy_list[i]];
}
}
})
.always(function () { .always(function () {
start(); start();
}); });
...@@ -954,7 +947,7 @@ ...@@ -954,7 +947,7 @@
test("spy indexedDB usage with one document", function () { test("spy indexedDB usage with one document", function () {
var context = this; var context = this;
stop(); stop();
expect(21); expect(22);
deleteIndexedDB(context.jio) deleteIndexedDB(context.jio)
.then(function () { .then(function () {
...@@ -972,6 +965,7 @@ ...@@ -972,6 +965,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete"); context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete");
context.spy_key_range = sinon.spy(IDBKeyRange, "only"); context.spy_key_range = sinon.spy(IDBKeyRange, "only");
...@@ -1017,9 +1011,11 @@ ...@@ -1017,9 +1011,11 @@
deepEqual(context.spy_index.secondCall.args[0], "_id", deepEqual(context.spy_index.secondCall.args[0], "_id",
"index first argument"); "index first argument");
ok(context.spy_cursor.calledTwice, "cursor count " + equal(context.spy_cursor.callCount, 0, "cursor count " +
context.spy_cursor.callCount); context.spy_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 0, "cursor count " + ok(context.spy_key_cursor.calledTwice, "cursor key count " +
context.spy_key_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 0, "cursor delete count " +
context.spy_cursor_delete.callCount); context.spy_cursor_delete.callCount);
ok(context.spy_key_range.calledTwice, "key range count " + ok(context.spy_key_range.calledTwice, "key range count " +
...@@ -1045,6 +1041,8 @@ ...@@ -1045,6 +1041,8 @@
delete context.spy_index; delete context.spy_index;
context.spy_create_index.restore(); context.spy_create_index.restore();
delete context.spy_create_index; delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore(); context.spy_cursor.restore();
delete context.spy_cursor; delete context.spy_cursor;
context.spy_cursor_delete.restore(); context.spy_cursor_delete.restore();
...@@ -1063,7 +1061,7 @@ ...@@ -1063,7 +1061,7 @@
test("spy indexedDB usage with 2 attachments", function () { test("spy indexedDB usage with 2 attachments", function () {
var context = this; var context = this;
stop(); stop();
expect(21); expect(26);
deleteIndexedDB(context.jio) deleteIndexedDB(context.jio)
.then(function () { .then(function () {
...@@ -1087,6 +1085,7 @@ ...@@ -1087,6 +1085,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete"); context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete");
context.spy_key_range = sinon.spy(IDBKeyRange, "only"); context.spy_key_range = sinon.spy(IDBKeyRange, "only");
...@@ -1119,10 +1118,18 @@ ...@@ -1119,10 +1118,18 @@
deepEqual(context.spy_store.thirdCall.args[0], "blob", deepEqual(context.spy_store.thirdCall.args[0], "blob",
"store first argument"); "store first argument");
equal(context.spy_delete.callCount, 1, "delete count " + equal(context.spy_delete.callCount, 5, "delete count " +
context.spy_delete.callCount); context.spy_delete.callCount);
deepEqual(context.spy_delete.firstCall.args[0], "foo", deepEqual(context.spy_delete.firstCall.args[0], "foo",
"delete first argument"); "delete first argument");
deepEqual(context.spy_delete.secondCall.args[0], "foo_attachment1",
"second delete first argument");
deepEqual(context.spy_delete.thirdCall.args[0], "foo_attachment1_0",
"third delete first argument");
deepEqual(context.spy_delete.getCall(3).args[0], "foo_attachment2",
"fourth delete first argument");
deepEqual(context.spy_delete.getCall(4).args[0], "foo_attachment2_0",
"fifth delete first argument");
ok(context.spy_index.calledTwice, "index count " + ok(context.spy_index.calledTwice, "index count " +
context.spy_index.callCount); context.spy_index.callCount);
...@@ -1131,10 +1138,12 @@ ...@@ -1131,10 +1138,12 @@
deepEqual(context.spy_index.secondCall.args[0], "_id", deepEqual(context.spy_index.secondCall.args[0], "_id",
"index first argument"); "index first argument");
ok(context.spy_cursor.calledTwice, "cursor count " + equal(context.spy_cursor.callCount, 0, "cursor count " +
context.spy_cursor.callCount); context.spy_cursor.callCount);
ok(context.spy_key_cursor.calledTwice, "cursor key count " +
context.spy_key_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 4, "cursor count " + equal(context.spy_cursor_delete.callCount, 0, "cursor count " +
context.spy_cursor_delete.callCount); context.spy_cursor_delete.callCount);
ok(context.spy_key_range.calledTwice, "key range count " + ok(context.spy_key_range.calledTwice, "key range count " +
...@@ -1160,6 +1169,8 @@ ...@@ -1160,6 +1169,8 @@
delete context.spy_index; delete context.spy_index;
context.spy_create_index.restore(); context.spy_create_index.restore();
delete context.spy_create_index; delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore(); context.spy_cursor.restore();
delete context.spy_cursor; delete context.spy_cursor;
context.spy_cursor_delete.restore(); context.spy_cursor_delete.restore();
...@@ -1212,6 +1223,8 @@ ...@@ -1212,6 +1223,8 @@
context.spy_index = sinon.spy(IDBObjectStore.prototype, "index"); context.spy_index = sinon.spy(IDBObjectStore.prototype, "index");
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
return context.jio.getAttachment("foo", attachment); return context.jio.getAttachment("foo", attachment);
}) })
...@@ -1241,17 +1254,24 @@ ...@@ -1241,17 +1254,24 @@
deepEqual(context.spy_store.secondCall.args[0], "blob", deepEqual(context.spy_store.secondCall.args[0], "blob",
"store first argument"); "store first argument");
equal(context.spy_get.callCount, 3, "get count " + equal(context.spy_get.callCount, 1, "get count " +
context.spy_get.callCount); context.spy_get.callCount);
deepEqual(context.spy_get.firstCall.args[0], "foo_attachment", deepEqual(context.spy_get.firstCall.args[0], "foo_attachment",
"get first argument"); "get first argument");
deepEqual(context.spy_get.secondCall.args[0], "foo_attachment_0",
"get first argument");
deepEqual(context.spy_get.thirdCall.args[0], "foo_attachment_1",
"get first argument");
ok(!context.spy_index.called, "index count " + ok(context.spy_index.called, "index count " +
context.spy_index.callCount); context.spy_index.callCount);
equal(context.spy_cursor.callCount, 1, "cursor count " +
context.spy_cursor.callCount);
ok(!context.spy_key_cursor.called, "cursor key count " +
context.spy_key_cursor.callCount);
ok(context.spy_key_range.calledOnce, "key range count " +
context.spy_key_range.callCount);
deepEqual(context.spy_key_range.firstCall.args[0],
["foo", "attachment"],
"key range first argument");
}) })
.always(function () { .always(function () {
context.spy_open.restore(); context.spy_open.restore();
...@@ -1268,6 +1288,10 @@ ...@@ -1268,6 +1288,10 @@
delete context.spy_index; delete context.spy_index;
context.spy_create_index.restore(); context.spy_create_index.restore();
delete context.spy_create_index; delete context.spy_create_index;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
}) })
.fail(function (error) { .fail(function (error) {
ok(false, error); ok(false, error);
...@@ -1299,7 +1323,7 @@ ...@@ -1299,7 +1323,7 @@
return jIO.util.readBlobAsText(result); return jIO.util.readBlobAsText(result);
}) })
.then(function (result) { .then(function (result) {
equal(result.target.result, big_string, ok(result.target.result === big_string,
"Attachment correctly fetched"); "Attachment correctly fetched");
}) })
.fail(function (error) { .fail(function (error) {
...@@ -1403,6 +1427,34 @@ ...@@ -1403,6 +1427,34 @@
start(); start();
}); });
}); });
test("non existing attachment", function () {
var context = this,
attachment = "attachment";
stop();
expect(3);
deleteIndexedDB(context.jio)
.then(function () {
return context.jio.put("foo", {"title": "bar"});
})
.then(function () {
return context.jio.getAttachment("foo", attachment);
})
.fail(function (error) {
ok(error instanceof jIO.util.jIOError);
equal(
error.message,
"IndexedDB: cannot find object 'foo_attachment' " +
"in the 'attachment' store"
);
equal(error.status_code, 404);
})
.always(function () {
start();
});
});
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// indexeddbStorage.removeAttachment // indexeddbStorage.removeAttachment
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -1419,7 +1471,7 @@ ...@@ -1419,7 +1471,7 @@
var context = this, var context = this,
attachment = "attachment"; attachment = "attachment";
stop(); stop();
expect(17); expect(20);
deleteIndexedDB(context.jio) deleteIndexedDB(context.jio)
.then(function () { .then(function () {
...@@ -1441,6 +1493,7 @@ ...@@ -1441,6 +1493,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete"); context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete");
context.spy_key_range = sinon.spy(IDBKeyRange, "only"); context.spy_key_range = sinon.spy(IDBKeyRange, "only");
...@@ -1473,17 +1526,23 @@ ...@@ -1473,17 +1526,23 @@
deepEqual(context.spy_store.secondCall.args[0], "blob", deepEqual(context.spy_store.secondCall.args[0], "blob",
"store first argument"); "store first argument");
equal(context.spy_delete.callCount, 1, "delete count " + equal(context.spy_delete.callCount, 3, "delete count " +
context.spy_delete.callCount); context.spy_delete.callCount);
deepEqual(context.spy_delete.firstCall.args[0], "foo_attachment", deepEqual(context.spy_delete.firstCall.args[0], "foo_attachment",
"delete first argument"); "delete first argument");
deepEqual(context.spy_delete.secondCall.args[0], "foo_attachment_0",
"second delete first argument");
deepEqual(context.spy_delete.thirdCall.args[0], "foo_attachment_1",
"third delete first argument");
ok(context.spy_index.calledOnce, "index count " + ok(context.spy_index.calledOnce, "index count " +
context.spy_index.callCount); context.spy_index.callCount);
ok(context.spy_cursor.calledOnce, "cursor count " + equal(context.spy_cursor.callCount, 0, "cursor count " +
context.spy_cursor.callCount); context.spy_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 2, "cursor count " + ok(context.spy_key_cursor.calledOnce, "cursor key count " +
context.spy_key_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 0, "cursor count " +
context.spy_cursor_delete.callCount); context.spy_cursor_delete.callCount);
ok(context.spy_key_range.calledOnce, "key range count " + ok(context.spy_key_range.calledOnce, "key range count " +
...@@ -1509,6 +1568,8 @@ ...@@ -1509,6 +1568,8 @@
delete context.spy_create_index; delete context.spy_create_index;
context.spy_cursor.restore(); context.spy_cursor.restore();
delete context.spy_cursor; delete context.spy_cursor;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor_delete.restore(); context.spy_cursor_delete.restore();
delete context.spy_cursor_delete; delete context.spy_cursor_delete;
context.spy_key_range.restore(); context.spy_key_range.restore();
...@@ -1538,12 +1599,15 @@ ...@@ -1538,12 +1599,15 @@
var context = this, var context = this,
attachment = "attachment"; attachment = "attachment";
stop(); stop();
expect(23); expect(18);
deleteIndexedDB(context.jio) deleteIndexedDB(context.jio)
.then(function () { .then(function () {
return context.jio.put("foo", {"title": "bar"}); return context.jio.put("foo", {"title": "bar"});
}) })
.then(function () {
return context.jio.putAttachment("foo", attachment, big_string);
})
.then(function () { .then(function () {
context.spy_open = sinon.spy(indexedDB, "open"); context.spy_open = sinon.spy(indexedDB, "open");
context.spy_create_store = sinon.spy(IDBDatabase.prototype, context.spy_create_store = sinon.spy(IDBDatabase.prototype,
...@@ -1557,10 +1621,14 @@ ...@@ -1557,10 +1621,14 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype, context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex"); "createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor"); context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete"); context.spy_cursor_delete = sinon.spy(IDBCursor.prototype, "delete");
context.spy_key_range = sinon.spy(IDBKeyRange, "only"); context.spy_key_range = sinon.spy(IDBKeyRange, "only");
return context.jio.putAttachment("foo", attachment, big_string); return context.jio.putAttachment("foo", attachment, 'small_string');
})
.fail(function (error) {
ok(false, error);
}) })
.then(function () { .then(function () {
...@@ -1582,36 +1650,27 @@ ...@@ -1582,36 +1650,27 @@
equal(context.spy_transaction.firstCall.args[1], "readwrite", equal(context.spy_transaction.firstCall.args[1], "readwrite",
"transaction second argument"); "transaction second argument");
equal(context.spy_store.callCount, 4, "store count " + equal(context.spy_store.callCount, 2, "store count " +
context.spy_store.callCount); context.spy_store.callCount);
deepEqual(context.spy_store.firstCall.args[0], "attachment", deepEqual(context.spy_store.firstCall.args[0], "attachment",
"store first argument"); "store first argument");
deepEqual(context.spy_store.secondCall.args[0], "blob", deepEqual(context.spy_store.secondCall.args[0], "blob",
"store first argument"); "store first argument");
deepEqual(context.spy_store.thirdCall.args[0], "attachment",
"store first argument");
deepEqual(context.spy_store.getCall(3).args[0], "blob",
"store first argument");
equal(context.spy_delete.callCount, 1, "delete count " + equal(context.spy_delete.callCount, 1, "delete count " +
context.spy_delete.callCount); context.spy_delete.callCount);
deepEqual(context.spy_delete.firstCall.args[0], "foo_attachment", deepEqual(context.spy_delete.firstCall.args[0], "foo_attachment_1",
"delete first argument"); "delete first argument");
ok(context.spy_index.calledOnce, "index count " + equal(context.spy_index.callCount, 1, "index count " +
context.spy_index.callCount); context.spy_index.callCount);
ok(context.spy_cursor.calledOnce, "cursor count " + equal(context.spy_cursor.callCount, 0, "cursor count " +
context.spy_cursor.callCount); context.spy_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 0, "delete count " + equal(context.spy_key_cursor.callCount, 1, "cursor count " +
context.spy_cursor_delete.callCount); context.spy_key_cursor.callCount);
ok(context.spy_key_range.calledOnce, "key range count " +
context.spy_key_range.callCount);
deepEqual(context.spy_key_range.firstCall.args[0],
["foo", "attachment"],
"key range first argument");
equal(context.spy_put.callCount, 3, "put count " + equal(context.spy_put.callCount, 2, "put count " +
context.spy_put.callCount); context.spy_put.callCount);
deepEqual(context.spy_put.firstCall.args[0], { deepEqual(context.spy_put.firstCall.args[0], {
"_attachment": "attachment", "_attachment": "attachment",
...@@ -1619,7 +1678,7 @@ ...@@ -1619,7 +1678,7 @@
"_key_path": "foo_attachment", "_key_path": "foo_attachment",
"info": { "info": {
"content_type": "text/plain;charset=utf-8", "content_type": "text/plain;charset=utf-8",
"length": 3000000 "length": 12
} }
}, "put first argument"); }, "put first argument");
delete context.spy_put.secondCall.args[0].blob; delete context.spy_put.secondCall.args[0].blob;
...@@ -1631,13 +1690,6 @@ ...@@ -1631,13 +1690,6 @@
"_key_path": "foo_attachment_0" "_key_path": "foo_attachment_0"
}, "put first argument"); }, "put first argument");
delete context.spy_put.thirdCall.args[0].blob; delete context.spy_put.thirdCall.args[0].blob;
// XXX Check blob content
deepEqual(context.spy_put.thirdCall.args[0], {
"_attachment": "attachment",
"_id": "foo",
"_part": 1,
"_key_path": "foo_attachment_1"
}, "put first argument");
}) })
.always(function () { .always(function () {
context.spy_open.restore(); context.spy_open.restore();
...@@ -1656,6 +1708,8 @@ ...@@ -1656,6 +1708,8 @@
delete context.spy_create_index; delete context.spy_create_index;
context.spy_cursor.restore(); context.spy_cursor.restore();
delete context.spy_cursor; delete context.spy_cursor;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor_delete.restore(); context.spy_cursor_delete.restore();
delete context.spy_cursor_delete; delete context.spy_cursor_delete;
context.spy_key_range.restore(); context.spy_key_range.restore();
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment