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 @@
store.createIndex("_id", "_id", {unique: false});
}
function openIndexedDB(jio_storage) {
var db_name = jio_storage._database_name;
function waitForOpenIndexedDB(db_name, callback) {
function resolver(resolve, reject) {
// Open DB //
var request = indexedDB.open(db_name);
......@@ -139,47 +138,86 @@
};
request.onsuccess = function () {
resolve(request.result);
};
}
// XXX Canceller???
return new RSVP.Queue()
.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);
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 () {
db.close();
return new RSVP.Queue()
.push(function () {
return result;
})
.push(resolve, function (error) {
canceller();
reject(error);
});
};
}
tx.onabort = function () {
db.close();
tx.onerror = function (error) {
canceller();
reject(error);
};
tx.onabort = function (evt) {
reject(evt.target);
};
return tx;
}
return new RSVP.Promise(resolver, canceller);
}
function handleCursor(request, callback, resolve, reject) {
request.onerror = function (error) {
if (request.transaction) {
request.transaction.abort();
function waitForIDBRequest(request) {
return new RSVP.Promise(function (resolve, reject) {
request.onerror = reject;
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) {
var cursor = evt.target.result;
if (cursor) {
// XXX Wait for result
if (cursor && !force_cancellation) {
try {
callback(cursor);
} catch (error) {
reject(error);
}
// continue to next iteration
cursor["continue"]();
} else {
......@@ -187,9 +225,12 @@
}
};
}
return new RSVP.Promise(resolver, canceller);
}
IndexedDBStorage.prototype.buildQuery = function (options) {
var result_list = [];
var result_list = [],
context = this;
function pushIncludedMetadata(cursor) {
result_list.push({
......@@ -205,17 +246,23 @@
"value": {}
});
}
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var tx = openTransaction(db, ["metadata"], "readonly");
return new RSVP.Queue()
.push(function () {
return waitForOpenIndexedDB(context._database_name, function (db) {
return waitForTransaction(db, ["metadata"], "readonly",
function (tx) {
if (options.include_docs === true) {
handleCursor(tx.objectStore("metadata").index("_id").openCursor(),
pushIncludedMetadata, resolve, reject);
} else {
handleCursor(tx.objectStore("metadata").index("_id")
.openKeyCursor(), pushMetadata, resolve, reject);
return waitForAllSynchronousCursor(
tx.objectStore("metadata").index("_id").openCursor(),
pushIncludedMetadata
);
}
return waitForAllSynchronousCursor(
tx.objectStore("metadata").index("_id").openKeyCursor(),
pushMetadata
);
});
});
})
.push(function () {
......@@ -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) {
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var transaction = openTransaction(db, ["metadata"], "readonly");
handleGet(
transaction.objectStore("metadata"),
id,
resolve,
reject
);
var context = this;
return new RSVP.Queue()
.push(function () {
return waitForOpenIndexedDB(context._database_name, function (db) {
return waitForTransaction(db, ["metadata"], "readonly",
function (tx) {
return waitForIDBRequest(tx.objectStore("metadata").get(id));
});
});
})
.push(function (result) {
return result.doc;
.push(function (evt) {
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) {
var attachment_dict = {};
var attachment_dict = {},
context = this;
function addEntry(cursor) {
attachment_dict[cursor.value._attachment] = {};
attachment_dict[cursor.primaryKey.slice(cursor.key.length + 1)] = {};
}
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var transaction = openTransaction(db, ["metadata", "attachment"],
"readonly");
function getAttachments() {
handleCursor(
transaction.objectStore("attachment").index("_id")
.openCursor(IDBKeyRange.only(id)),
addEntry,
resolve,
reject
);
}
handleGet(
transaction.objectStore("metadata"),
id,
getAttachments,
reject
);
return new RSVP.Queue()
.push(function () {
return waitForOpenIndexedDB(context._database_name, function (db) {
return waitForTransaction(db, ["metadata", "attachment"], "readonly",
function (tx) {
return RSVP.all([
waitForIDBRequest(tx.objectStore("metadata").get(id)),
waitForAllSynchronousCursor(
tx.objectStore("attachment").index("_id")
.openKeyCursor(IDBKeyRange.only(id)),
addEntry
)
]);
});
});
})
.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;
});
};
function handleRequest(request, resolve, reject) {
request.onerror = reject;
request.onsuccess = function () {
resolve(request.result);
};
}
IndexedDBStorage.prototype.put = function (id, metadata) {
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
var transaction = openTransaction(db, ["metadata"], "readwrite");
handleRequest(
transaction.objectStore("metadata").put({
return waitForOpenIndexedDB(this._database_name, function (db) {
return waitForTransaction(db, ["metadata"], "readwrite",
function (tx) {
return waitForIDBRequest(tx.objectStore("metadata").put({
"_id": id,
"doc": metadata
}),
resolve,
reject
);
}));
});
});
};
function deleteEntry(cursor) {
cursor["delete"]();
}
IndexedDBStorage.prototype.remove = function (id) {
var resolved_amount = 0;
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
function resolver() {
if (resolved_amount < 2) {
resolved_amount += 1;
} else {
resolve();
}
}
var transaction = openTransaction(db, ["metadata", "attachment",
"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
return waitForOpenIndexedDB(this._database_name, function (db) {
return waitForTransaction(db, ["metadata", "attachment", "blob"],
"readwrite", function (tx) {
var promise_list = [],
metadata_store = tx.objectStore("metadata"),
attachment_store = tx.objectStore("attachment"),
blob_store = tx.objectStore("blob");
function deleteAttachment(cursor) {
promise_list.push(
waitForIDBRequest(attachment_store.delete(cursor.primaryKey))
);
handleCursor(transaction.objectStore("blob").index("_id")
.openCursor(IDBKeyRange.only(id)),
deleteEntry,
resolver,
reject
}
function deleteBlob(cursor) {
promise_list.push(
waitForIDBRequest(blob_store.delete(cursor.primaryKey))
);
}
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) {
var transaction,
type,
start,
end;
if (options === undefined) {
options = {};
}
return openIndexedDB(this)
.push(function (db) {
return new RSVP.Promise(function (resolve, reject) {
transaction = openTransaction(
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;
var db_name = this._database_name,
start,
end,
array_buffer_list = [];
start = options.start || 0;
end = options.end || total_length;
if (end > total_length) {
end = total_length;
}
if (start < 0 || end < 0) {
end = options.end;
// Stream the blob content
if ((start !== 0) || (end !== undefined)) {
if (start < 0 || ((end !== undefined) && (end < 0))) {
throw new jIO.util.jIOError(
"_start and _end must be positive",
400
);
}
if (start > end) {
if ((end !== undefined) && (start > end)) {
throw new jIO.util.jIOError("_start is greater than _end",
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);
end_index = Math.floor(end / UNITE) - 1;
if (end !== undefined) {
end_index = Math.floor(end / UNITE);
if (end % UNITE === 0) {
end_index -= 1;
}
function resolver(result) {
if (result.blob !== undefined) {
result_list.push(result);
}
resolve(result_list);
}
function getPart(i) {
return function (result) {
if (result) {
result_list.push(result);
function getBlobKey(cursor) {
var index = parseInt(
cursor.primaryKey.slice(key_path.length + 1),
10
),
i;
if ((start !== 0) && (index < start_index)) {
// No need to fetch blobs at the start
return;
}
i += 1;
handleGet(store,
buildKeyPath([id, name, i]),
(i <= end_index) ? getPart(i) : resolver,
reject
);
};
if ((end !== undefined) && (index > end_index)) {
// No need to fetch blobs at the end
return;
}
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
handleGet(transaction.objectStore("attachment"),
buildKeyPath([id, name]),
getBlob,
reject
// Sort the blob by their index
promise_list.splice(
index - start_index,
0,
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) {
var array_buffer_list = [],
blob,
i,
// No need to keep the IDB open
var blob,
index,
len = result_list.length;
for (i = 0; i < len; i += 1) {
array_buffer_list.push(result_list[i].blob);
}
if ((options.start === undefined) && (options.end === undefined)) {
return new Blob(array_buffer_list, {type: type});
i;
for (i = 0; i < result_list.length; i += 1) {
array_buffer_list.push(result_list[i].target.result.blob);
}
blob = new Blob(array_buffer_list,
{type: "application/octet-stream"});
index = Math.floor(start / UNITE) * UNITE;
blob = new Blob(array_buffer_list, {type: "application/octet-stream"});
return blob.slice(start - index, end - index,
if (end === undefined) {
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
);
}
function removeAttachment(transaction, id, name, resolve, reject) {
// XXX How to get the right attachment
function deleteContent() {
handleCursor(
transaction.objectStore("blob").index("_id_attachment")
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])),
deleteEntry,
resolve,
reject
getBlob
)
]);
});
});
})
.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"](
buildKeyPath([id, name])
),
deleteContent,
reject
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
);
}
return blob;
});
};
IndexedDBStorage.prototype.putAttachment = function (id, name, blob) {
var blob_part = [],
transaction,
db;
return openIndexedDB(this)
.push(function (database) {
db = database;
var db_name = this._database_name;
return new RSVP.Queue()
.push(function () {
// Split the blob first
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (event) {
var array_buffer = event.target.result,
blob_part = [],
total_size = blob.size,
handled_size = 0;
......@@ -489,55 +586,100 @@
handled_size += UNITE;
}
// Remove previous attachment
transaction = openTransaction(db, ["attachment", "blob"], "readwrite");
return new RSVP.Promise(function (resolve, reject) {
function write() {
var len = blob_part.length - 1,
attachment_store = transaction.objectStore("attachment"),
blob_store = transaction.objectStore("blob");
function putBlobPart(i) {
return function () {
i += 1;
handleRequest(
blob_store.put({
return waitForOpenIndexedDB(db_name, function (db) {
return waitForTransaction(db, ["attachment", "blob"], "readwrite",
function (tx) {
var blob_store,
promise_list,
delete_promise_list = [],
key_path = buildKeyPath([id, name]),
i;
// First write the attachment info on top of previous
promise_list = [
waitForIDBRequest(tx.objectStore("attachment").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]),
"_id" : id,
"_attachment" : name,
"_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),
reject
// Finally, remove all remaining blobs
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) {
return openIndexedDB(this)
.push(function (db) {
var transaction = openTransaction(db, ["attachment", "blob"],
"readwrite");
return new RSVP.Promise(function (resolve, reject) {
removeAttachment(transaction, id, name, resolve, reject);
return waitForOpenIndexedDB(this._database_name, function (db) {
return waitForTransaction(db, ["attachment", "blob"], "readwrite",
function (tx) {
var promise_list = [],
attachment_store = tx.objectStore("attachment"),
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 @@
}
function deleteIndexedDB(storage) {
return new RSVP.Queue()
.push(function () {
function resolver(resolve, reject) {
return new RSVP.Promise(function resolver(resolve, reject) {
var request = indexedDB.deleteDatabase(
storage.__storage._database_name
);
request.onerror = reject;
request.onblocked = reject;
request.onsuccess = resolve;
}
return new RSVP.Promise(resolver);
});
}
......@@ -597,7 +593,7 @@
test("spy indexedDB usage", function () {
var context = this;
stop();
expect(17);
expect(18);
deleteIndexedDB(context.jio)
.then(function () {
......@@ -615,6 +611,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
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");
return context.jio.allAttachments("foo");
......@@ -654,8 +651,10 @@
deepEqual(context.spy_index.firstCall.args[0], "_id",
"index first argument");
ok(context.spy_cursor.calledOnce, "cursor count " +
ok(!context.spy_cursor.called, "cursor count " +
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 " +
context.spy_key_range.callCount);
......@@ -677,6 +676,8 @@
delete context.spy_index;
context.spy_create_index.restore();
delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_key_range.restore();
......@@ -891,29 +892,21 @@
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) {
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 () {
start();
});
......@@ -954,7 +947,7 @@
test("spy indexedDB usage with one document", function () {
var context = this;
stop();
expect(21);
expect(22);
deleteIndexedDB(context.jio)
.then(function () {
......@@ -972,6 +965,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
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_key_range = sinon.spy(IDBKeyRange, "only");
......@@ -1017,9 +1011,11 @@
deepEqual(context.spy_index.secondCall.args[0], "_id",
"index first argument");
ok(context.spy_cursor.calledTwice, "cursor count " +
equal(context.spy_cursor.callCount, 0, "cursor count " +
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);
ok(context.spy_key_range.calledTwice, "key range count " +
......@@ -1045,6 +1041,8 @@
delete context.spy_index;
context.spy_create_index.restore();
delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_cursor_delete.restore();
......@@ -1063,7 +1061,7 @@
test("spy indexedDB usage with 2 attachments", function () {
var context = this;
stop();
expect(21);
expect(26);
deleteIndexedDB(context.jio)
.then(function () {
......@@ -1087,6 +1085,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
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_key_range = sinon.spy(IDBKeyRange, "only");
......@@ -1119,10 +1118,18 @@
deepEqual(context.spy_store.thirdCall.args[0], "blob",
"store first argument");
equal(context.spy_delete.callCount, 1, "delete count " +
equal(context.spy_delete.callCount, 5, "delete count " +
context.spy_delete.callCount);
deepEqual(context.spy_delete.firstCall.args[0], "foo",
"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 " +
context.spy_index.callCount);
......@@ -1131,10 +1138,12 @@
deepEqual(context.spy_index.secondCall.args[0], "_id",
"index first argument");
ok(context.spy_cursor.calledTwice, "cursor count " +
equal(context.spy_cursor.callCount, 0, "cursor count " +
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);
ok(context.spy_key_range.calledTwice, "key range count " +
......@@ -1160,6 +1169,8 @@
delete context.spy_index;
context.spy_create_index.restore();
delete context.spy_create_index;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_cursor_delete.restore();
......@@ -1212,6 +1223,8 @@
context.spy_index = sinon.spy(IDBObjectStore.prototype, "index");
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
context.spy_cursor = sinon.spy(IDBIndex.prototype, "openCursor");
context.spy_key_cursor = sinon.spy(IDBIndex.prototype, "openKeyCursor");
return context.jio.getAttachment("foo", attachment);
})
......@@ -1241,17 +1254,24 @@
deepEqual(context.spy_store.secondCall.args[0], "blob",
"store first argument");
equal(context.spy_get.callCount, 3, "get count " +
equal(context.spy_get.callCount, 1, "get count " +
context.spy_get.callCount);
deepEqual(context.spy_get.firstCall.args[0], "foo_attachment",
"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);
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 () {
context.spy_open.restore();
......@@ -1268,6 +1288,10 @@
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_cursor.restore();
delete context.spy_key_cursor;
})
.fail(function (error) {
ok(false, error);
......@@ -1299,7 +1323,7 @@
return jIO.util.readBlobAsText(result);
})
.then(function (result) {
equal(result.target.result, big_string,
ok(result.target.result === big_string,
"Attachment correctly fetched");
})
.fail(function (error) {
......@@ -1403,6 +1427,34 @@
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
/////////////////////////////////////////////////////////////////
......@@ -1419,7 +1471,7 @@
var context = this,
attachment = "attachment";
stop();
expect(17);
expect(20);
deleteIndexedDB(context.jio)
.then(function () {
......@@ -1441,6 +1493,7 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
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_key_range = sinon.spy(IDBKeyRange, "only");
......@@ -1473,17 +1526,23 @@
deepEqual(context.spy_store.secondCall.args[0], "blob",
"store first argument");
equal(context.spy_delete.callCount, 1, "delete count " +
equal(context.spy_delete.callCount, 3, "delete count " +
context.spy_delete.callCount);
deepEqual(context.spy_delete.firstCall.args[0], "foo_attachment",
"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 " +
context.spy_index.callCount);
ok(context.spy_cursor.calledOnce, "cursor count " +
equal(context.spy_cursor.callCount, 0, "cursor count " +
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);
ok(context.spy_key_range.calledOnce, "key range count " +
......@@ -1509,6 +1568,8 @@
delete context.spy_create_index;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor_delete.restore();
delete context.spy_cursor_delete;
context.spy_key_range.restore();
......@@ -1538,12 +1599,15 @@
var context = this,
attachment = "attachment";
stop();
expect(23);
expect(18);
deleteIndexedDB(context.jio)
.then(function () {
return context.jio.put("foo", {"title": "bar"});
})
.then(function () {
return context.jio.putAttachment("foo", attachment, big_string);
})
.then(function () {
context.spy_open = sinon.spy(indexedDB, "open");
context.spy_create_store = sinon.spy(IDBDatabase.prototype,
......@@ -1557,10 +1621,14 @@
context.spy_create_index = sinon.spy(IDBObjectStore.prototype,
"createIndex");
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_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 () {
......@@ -1582,36 +1650,27 @@
equal(context.spy_transaction.firstCall.args[1], "readwrite",
"transaction second argument");
equal(context.spy_store.callCount, 4, "store count " +
equal(context.spy_store.callCount, 2, "store count " +
context.spy_store.callCount);
deepEqual(context.spy_store.firstCall.args[0], "attachment",
"store first argument");
deepEqual(context.spy_store.secondCall.args[0], "blob",
"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 " +
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");
ok(context.spy_index.calledOnce, "index count " +
equal(context.spy_index.callCount, 1, "index count " +
context.spy_index.callCount);
ok(context.spy_cursor.calledOnce, "cursor count " +
equal(context.spy_cursor.callCount, 0, "cursor count " +
context.spy_cursor.callCount);
equal(context.spy_cursor_delete.callCount, 0, "delete count " +
context.spy_cursor_delete.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_key_cursor.callCount, 1, "cursor count " +
context.spy_key_cursor.callCount);
equal(context.spy_put.callCount, 3, "put count " +
equal(context.spy_put.callCount, 2, "put count " +
context.spy_put.callCount);
deepEqual(context.spy_put.firstCall.args[0], {
"_attachment": "attachment",
......@@ -1619,7 +1678,7 @@
"_key_path": "foo_attachment",
"info": {
"content_type": "text/plain;charset=utf-8",
"length": 3000000
"length": 12
}
}, "put first argument");
delete context.spy_put.secondCall.args[0].blob;
......@@ -1631,13 +1690,6 @@
"_key_path": "foo_attachment_0"
}, "put first argument");
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 () {
context.spy_open.restore();
......@@ -1656,6 +1708,8 @@
delete context.spy_create_index;
context.spy_cursor.restore();
delete context.spy_cursor;
context.spy_key_cursor.restore();
delete context.spy_key_cursor;
context.spy_cursor_delete.restore();
delete context.spy_cursor_delete;
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