Skip to content

Commit

Permalink
[IndexedDB]: Implement explicit commit() in renderer
Browse files Browse the repository at this point in the history
Implementing an explicit commit function for the IndexedDB Transaction API.
The addition of this API allows developers to preempt IndexedDB's autocommit
functionality by sending their own explicit commit signal.

Explainer: https://andreas-butler.github.io/idb-transaction-commit/EXPLAINER
Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=898257
Change-Id: I5e8bdc697052139d014757408d9a7f2b6367655b
  • Loading branch information
andreas-butler authored and chromium-wpt-export-bot committed Nov 29, 2018
1 parent 3afcfd2 commit a5672e0
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 10 deletions.
45 changes: 45 additions & 0 deletions IndexedDB/idb-explicit-commit-throw.any.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// META: script=support-promises.js

/**
* This file contains a test that was separated out from the rest of the idb
* explict commit tests because it requires the flag 'allow_uncaught_exception',
* which prevents unintentionally thrown errors from failing tests.
*
* @author [email protected]
*/

setup({allow_uncaught_exception:true});

promise_test(async testCase => {
// Register an event listener that will prevent the intentionally thrown
// error from bubbling up to the window and failing the testharness. This
// is necessary because currently allow_uncaught_exception does not behave
// as expected for promise_test.
//
// Git issue: https://github.com/web-platform-tests/wpt/issues/14041
self.addEventListener('error', (event) => { event.preventDefault(); });

const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});

const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
const putRequest = objectStore.put({isbn:'one', title:'title'});
txn.commit();
putRequest.onsuccess = () => {
throw new Error('This error thrown after an explicit commit should not ' +
'prevent the transaction from committing.');
}
await promiseForTransaction(testCase, txn);

// Ensure that despite the uncaught error after the put request, the explicit
// commit still causes the request to be committed.
const txn2 = db.transaction(['books'], 'readwrite');
const objectStore2 = txn2.objectStore('books');
const getRequest = objectStore2.get('one');
await promiseForTransaction(testCase, txn2);

assert_equals(getRequest.result.title, 'title');
}, 'Any errors in callbacks that run after an explicit commit will not stop '
+ 'the commit from being processed.');
181 changes: 181 additions & 0 deletions IndexedDB/idb-explicit-commit.any.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
// META: script=support-promises.js

/**
* This file contains the webplatform tests for the explicit commit() method
* of the IndexedDB transaction API.
*
* @author [email protected]
*/

promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
objectStore.put({isbn: 'one', title: 'title1'});
objectStore.put({isbn: 'two', title: 'title2'});
objectStore.put({isbn: 'three', title: 'title3'});
txn.commit();
await promiseForTransaction(testCase, txn);

const txn2 = db.transaction(['books'], 'readonly');
const objectStore2 = txn2.objectStore('books');
const getRequestitle1 = objectStore2.get('one');
const getRequestitle2 = objectStore2.get('two');
const getRequestitle3 = objectStore2.get('three');
txn2.commit();
await promiseForTransaction(testCase, txn2);
assert_array_equals(
[getRequestitle1.result.title,
getRequestitle2.result.title,
getRequestitle3.result.title],
['title1', 'title2', 'title3'],
'All three retrieved titles should match those that were put.');
db.close();
}, 'Explicitly committed data can be read back out.');


promise_test(async testCase => {
let db = await createDatabase(testCase, () => {});
assert_equals(1, db.version, 'A database should be created as version 1');
db.close();

// Upgrade the versionDB database and explicitly commit its versionchange
// transaction.
db = await migrateDatabase(testCase, 2, async (db, txn) => {
txn.commit();
});
assert_equals(2, db.version,
'The database version should have been incremented regardless of '
+ 'whether the versionchange transaction was explicitly or implicitly '
+ 'committed.');
db.close();
}, 'commit() on a version change transaction does not cause errors.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
txn.commit();
assert_throws('TransactionInactiveError',
() => { objectStore.put({isbn: 'one', title: 'title1'}); },
'After commit is called, the transaction should be inactive.');
db.close();
}, 'A committed transaction becomes inactive immediately.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
const putRequest = objectStore.put({isbn: 'one', title: 'title1'});
putRequest.onsuccess = testCase.step_func(() => {
assert_throws('TransactionInactiveError',
() => { objectStore.put({isbn:'two', title:'title2'}); },
'The transaction should not be active in the callback of a request after '
+ 'commit() is called.');
});
txn.commit();
await promiseForTransaction(testCase, txn);
db.close();
}, 'A committed transaction is inactive in future request callbacks.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
txn.commit();

assert_throws('TransactionInactiveError',
() => { objectStore.put({isbn:'one', title:'title1'}); },
'After commit is called, the transaction should be inactive.');

const txn2 = db.transaction(['books'], 'readonly');
const objectStore2 = txn2.objectStore('books');
const getRequest = objectStore2.get('one');
await promiseForTransaction(testCase, txn2);
assert_equals(getRequest.result, undefined);

db.close();
}, 'Puts issued after commit are not fulfilled.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
txn.abort();
assert_throws('InvalidStateError',
() => { txn.commit(); },
'The transaction should have been aborted.');
db.close();
}, 'Calling commit on an aborted transaction throws.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
txn.commit();
assert_throws('InvalidStateError',
() => { txn.commit(); },
'The transaction should have already committed.');
db.close();
}, 'Calling commit on a committed transaction throws.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
const putRequest = objectStore.put({isbn:'one', title:'title1'});
txn.commit();
assert_throws('InvalidStateError',
() => { txn.abort(); },
'The transaction should already have committed.');
const txn2 = db.transaction(['books'], 'readwrite');
const objectStore2 = txn2.objectStore('books');
const getRequest = objectStore2.get('one');
await promiseForTransaction(testCase, txn2);
assert_equals(
getRequest.result.title,
'title1',
'Explicitly committed data should be gettable.');
db.close();
}, 'Calling abort on a committed transaction throws and does not prevent '
+ 'persisting the data.');


promise_test(async testCase => {
const db = await createDatabase(testCase, async db => {
await createBooksStore(testCase, db);
});
const txn = db.transaction(['books'], 'readwrite');
const objectStore = txn.objectStore('books');
const releaseTxnFunction = keepAlive(testCase, txn, 'books');

// Break up the scope of execution to force the transaction into an inactive
// state.
await timeoutPromise(0);

assert_throws('InvalidStateError',
() => { txn.commit(); },
'The transaction should be inactive so calling commit should throw.');
releaseTxnFunction();
db.close();
}, 'Calling txn.commit() when txn is inactive should throw.');
29 changes: 29 additions & 0 deletions IndexedDB/support-promises.js
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,32 @@ async function deleteAllDatabases(testCase) {
await eventWatcher.wait_for('success');
}
}

// Keeps the passed transaction alive indefinitely (by making requests
// against the named store). Returns a function that asserts that the
// transaction has not already completed and then ends the request loop so that
// the transaction may autocommit and complete.
function keepAlive(testCase, transaction, storeName) {
let completed = false;
transaction.addEventListener('complete', () => { completed = true; });

let keepSpinning = true;

function spin() {
if (!keepSpinning)
return;
transaction.objectStore(storeName).get(0).onsuccess = spin;
}
spin();

return testCase.step_func(() => {
assert_false(completed, 'Transaction completed while kept alive');
keepSpinning = false;
});
}

// Return a promise that resolves after a setTimeout finishes to break up the
// scope of a function's execution.
function timeoutPromise(ms) {
return new Promise(resolve => { setTimeout(resolve, ms); });
}
17 changes: 7 additions & 10 deletions IndexedDB/support.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
var databaseName = "database";
var databaseVersion = 1;

/* Delete created databases
*
* Go through each finished test, see if it has an associated database. Close
Expand Down Expand Up @@ -170,25 +167,25 @@ function is_transaction_active(tx, store_name) {
}
}

// Keep the passed transaction alive indefinitely (by making requests
// against the named store). Returns a function to to let the
// transaction finish, and asserts that the transaction is not yet
// finished.
// Keeps the passed transaction alive indefinitely (by making requests
// against the named store). Returns a function that asserts that the
// transaction has not already completed and then ends the request loop so that
// the transaction may autocommit and complete.
function keep_alive(tx, store_name) {
let completed = false;
tx.addEventListener('complete', () => { completed = true; });

let pin = true;
let keepSpinning = true;

function spin() {
if (!pin)
if (!keepSpinning)
return;
tx.objectStore(store_name).get(0).onsuccess = spin;
}
spin();

return () => {
assert_false(completed, 'Transaction completed while kept alive');
pin = false;
keepSpinning = false;
};
}

0 comments on commit a5672e0

Please sign in to comment.