Commit 8ab4f278 authored by Romain Courteaud's avatar Romain Courteaud

[indexedDBStorage] handle transaction event

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.

As transaction abort will reject all accesses, it is safe to use .then method inside the transaction callback.

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 1da27f36
...@@ -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,57 +138,99 @@ ...@@ -139,57 +138,99 @@
}; };
request.onsuccess = function () { request.onsuccess = function () {
resolve(request.result); return new RSVP.Queue()
.push(function () {
return callback(request.result);
})
.push(function (result) {
request.result.close();
resolve(result);
}, function (error) {
request.result.close();
reject(error);
});
}; };
} }
// XXX Canceller???
return new RSVP.Queue() return new RSVP.Promise(resolver);
.push(function () {
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) {
canceller();
reject(error);
};
tx.onabort = function (evt) {
reject(evt.target);
};
return tx;
} }
tx.onabort = function () { return new RSVP.Promise(resolver, canceller);
db.close();
};
return tx;
} }
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); }
};
request.onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor) {
// XXX Wait for result
try {
callback(cursor);
} catch (error) {
reject(error);
}
// continue to next iteration function waitForAllSynchronousCursor(request, callback) {
cursor["continue"](); var force_cancellation = false;
} else {
resolve(); function canceller() {
} force_cancellation = true;
}; }
function resolver(resolve, reject) {
request.onerror = reject;
request.onsuccess = function (evt) {
var cursor = evt.target.result;
if (cursor && !force_cancellation) {
try {
callback(cursor);
} catch (error) {
reject(error);
}
// continue to next iteration
cursor["continue"]();
} else {
resolve();
}
};
}
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) {
if (options.include_docs === true) { return waitForTransaction(db, ["metadata"], "readonly",
handleCursor(tx.objectStore("metadata").index("_id").openCursor(), function (tx) {
pushIncludedMetadata, resolve, reject); if (options.include_docs === true) {
} else { return waitForAllSynchronousCursor(
handleCursor(tx.objectStore("metadata").index("_id") tx.objectStore("metadata").index("_id").openCursor(),
.openKeyCursor(), pushMetadata, resolve, reject); pushIncludedMetadata
} );
}
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( "_id": id,
transaction.objectStore("metadata").put({ "doc": metadata
"_id": id, }));
"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"], start = options.start || 0;
"readonly" end = options.end;
);
function getBlob(attachment) { // Stream the blob content
var total_length = attachment.info.length, if ((start !== 0) || (end !== undefined)) {
result_list = [],
store = transaction.objectStore("blob"), if (start < 0 || ((end !== undefined) && (end < 0))) {
start_index, throw new jIO.util.jIOError(
end_index; "_start and _end must be positive",
type = attachment.info.content_type; 400
start = options.start || 0; );
end = options.end || total_length; }
if (end > total_length) { if ((end !== undefined) && (start > end)) {
end = total_length; throw new jIO.util.jIOError("_start is greater than _end",
} 400);
if (start < 0 || end < 0) { }
throw new jIO.util.jIOError(
"_start and _end must be positive", return new RSVP.Queue()
400 .push(function () {
); return waitForOpenIndexedDB(db_name, function (db) {
} return waitForTransaction(db, ["blob"], "readonly",
if (start > end) { function (tx) {
throw new jIO.util.jIOError("_start is greater than _end", var key_path = buildKeyPath([id, name]),
400); blob_store = tx.objectStore("blob"),
} start_index,
start_index = Math.floor(start / UNITE); end_index,
end_index = Math.floor(end / UNITE) - 1; promise_list = [];
if (end % UNITE === 0) {
end_index -= 1;
} start_index = Math.floor(start / UNITE);
function resolver(result) { if (end !== undefined) {
if (result.blob !== undefined) { end_index = Math.floor(end / UNITE);
result_list.push(result); if (end % UNITE === 0) {
} end_index -= 1;
resolve(result_list); }
}
function getPart(i) {
return function (result) {
if (result) {
result_list.push(result);
} }
i += 1;
handleGet(store, function getBlobKey(cursor) {
buildKeyPath([id, name, i]), var index = parseInt(
(i <= end_index) ? getPart(i) : resolver, cursor.primaryKey.slice(key_path.length + 1),
reject 10
),
i;
if ((start !== 0) && (index < start_index)) {
// No need to fetch blobs at the start
return;
}
if ((end !== undefined) && (index > end_index)) {
// No need to fetch blobs at the end
return;
}
i = index - start_index;
// Extend array size
while (i > promise_list.length) {
promise_list.push(null);
i -= 1;
}
// Sort the blob by their index
promise_list.splice(
index - start_index,
0,
waitForIDBRequest(blob_store.get(cursor.primaryKey))
); );
}; }
}
getPart(start_index - 1)(); // 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) {
// No need to keep the IDB open
var blob,
index,
i;
for (i = 0; i < result_list.length; i += 1) {
array_buffer_list.push(result_list[i].target.result.blob);
} }
// XXX Should raise if key is not good blob = new Blob(array_buffer_list,
handleGet(transaction.objectStore("attachment"), {type: "application/octet-stream"});
buildKeyPath([id, name]), index = Math.floor(start / UNITE) * UNITE;
getBlob, if (end === undefined) {
reject end = blob.length;
); } else {
end = end - index;
}
return blob.slice(start - index, end,
"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
);
}
return RSVP.all([
// Get the attachment info (mime type)
waitForIDBRequest(attachment_store.get(
key_path
)),
// Get all needed blobs
waitForAllSynchronousCursor(
blob_store.index("_id_attachment")
.openCursor(IDBKeyRange.only([id, name])),
getBlob
)
]);
});
});
}) })
.push(function (result_list) { .push(function (result_list) {
var array_buffer_list = [], // No need to keep the IDB open
blob, var blob,
i, attachment = result_list[0].target.result;
index,
len = result_list.length; // Should raise if key is not good
for (i = 0; i < len; i += 1) { if (!attachment) {
array_buffer_list.push(result_list[i].blob); throw new jIO.util.jIOError(
"IndexedDB: cannot find object '" +
buildKeyPath([id, name]) +
"' in the 'attachment' store",
404
);
} }
if ((options.start === undefined) && (options.end === undefined)) {
return new Blob(array_buffer_list, {type: type}); blob = new Blob(array_buffer_list,
{type: attachment.info.content_type});
if (blob.length !== attachment.info.total_length) {
throw new jIO.util.jIOError(
"IndexedDB: attachment '" +
buildKeyPath([id, name]) +
"' in the 'attachment' store is broken",
500
);
} }
index = Math.floor(start / UNITE) * UNITE; return blob;
blob = new Blob(array_buffer_list, {type: "application/octet-stream"});
return blob.slice(start - index, end - index,
"application/octet-stream");
}); });
}; };
function removeAttachment(transaction, id, name, resolve, reject) {
// XXX How to get the right attachment
function deleteContent() {
handleCursor(
transaction.objectStore("blob").index("_id_attachment")
.openCursor(IDBKeyRange.only([id, name])),
deleteEntry,
resolve,
reject
);
}
handleRequest(
transaction.objectStore("attachment")["delete"](
buildKeyPath([id, name])
),
deleteContent,
reject
);
}
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,57 +586,102 @@ ...@@ -489,57 +586,102 @@
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
); );
}; }
}
handleRequest( function deleteEntry(cursor) {
attachment_store.put({ var index = parseInt(
"_key_path": buildKeyPath([id, name]), cursor.primaryKey.slice(key_path.length + 1),
"_id": id, 10
"_attachment": name, );
"info": { if (index >= blob_part.length) {
"content_type": blob.type, delete_promise_list.push(
"length": blob.size waitForIDBRequest(blob_store.delete(cursor.primaryKey))
);
} }
}), }
putBlobPart(-1),
reject // Finally, remove all remaining blobs
); promise_list.push(
} waitForAllSynchronousCursor(
removeAttachment(transaction, id, name, write, reject); 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);
}
});
});
}); });
}); });
}; };
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);
});
}); });
}); });
}; };
jIO.addStorage("indexeddb", IndexedDBStorage); jIO.addStorage("indexeddb", IndexedDBStorage);
......
...@@ -39,18 +39,14 @@ ...@@ -39,18 +39,14 @@
} }
function deleteIndexedDB(storage) { function deleteIndexedDB(storage) {
return new RSVP.Queue() return new RSVP.Promise(function resolver(resolve, reject) {
.push(function () { var request = indexedDB.deleteDatabase(
function resolver(resolve, reject) { storage.__storage._database_name
var request = indexedDB.deleteDatabase( );
storage.__storage._database_name request.onerror = reject;
); request.onblocked = reject;
request.onerror = reject; request.onsuccess = resolve;
request.onblocked = reject; });
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,8 +1323,8 @@ ...@@ -1299,8 +1323,8 @@
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) {
ok(false, error); ok(false, 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