245 lines
7 KiB
JavaScript
245 lines
7 KiB
JavaScript
// META: title=IndexedDB: transactions with large request results are aborted correctly
|
|
// META: global=window,worker
|
|
// META: script=resources/support-promises.js
|
|
// META: timeout=long
|
|
|
|
// Spec: https://w3c.github.io/IndexedDB/#abort-transaction
|
|
|
|
'use strict';
|
|
|
|
// Should be large enough to trigger large value handling in the IndexedDB
|
|
// engines that have special code paths for large values.
|
|
const wrapThreshold = 128 * 1024;
|
|
|
|
function populateStore(store) {
|
|
store.put({id: 1, key: 'k1', value: largeValue(wrapThreshold, 1)});
|
|
store.put({id: 2, key: 'k2', value: ['small-2']});
|
|
store.put({id: 3, key: 'k3', value: largeValue(wrapThreshold, 3)});
|
|
store.put({id: 4, key: 'k4', value: ['small-4']});
|
|
}
|
|
|
|
// Opens index cursors for operations that require open cursors.
|
|
//
|
|
// onsuccess is called if all cursors are opened successfully. Otherwise,
|
|
// onerror will be called at least once.
|
|
function openCursors(testCase, index, operations, onerror, onsuccess) {
|
|
let pendingCursors = 0;
|
|
|
|
for (let operation of operations) {
|
|
const opcode = operation[0];
|
|
const primaryKey = operation[1];
|
|
let request;
|
|
switch (opcode) {
|
|
case 'continue':
|
|
request =
|
|
index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey - 1}`));
|
|
break;
|
|
case 'continue-empty':
|
|
// k4 is the last key in the data set, so calling continue() will get
|
|
// the cursor past the end of the store.
|
|
request = index.openCursor(IDBKeyRange.lowerBound('k4'));
|
|
break;
|
|
default:
|
|
continue;
|
|
}
|
|
|
|
operation[2] = request;
|
|
++pendingCursors;
|
|
|
|
request.onsuccess = testCase.step_func(() => {
|
|
--pendingCursors;
|
|
if (!pendingCursors)
|
|
onsuccess();
|
|
});
|
|
request.onerror = testCase.step_func(onerror);
|
|
}
|
|
|
|
if (!pendingCursors)
|
|
onsuccess();
|
|
}
|
|
|
|
function doOperation(testCase, store, index, operation, requestId, results) {
|
|
const opcode = operation[0];
|
|
const primaryKey = operation[1];
|
|
const cursor = operation[2];
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let request;
|
|
switch (opcode) {
|
|
case 'add': // Tests returning a primary key.
|
|
request =
|
|
store.add({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
|
break;
|
|
case 'put': // Tests returning a primary key.
|
|
request =
|
|
store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
|
break;
|
|
case 'put-with-id': // Tests returning success or a primary key.
|
|
request = store.put({
|
|
key: `k${primaryKey}`,
|
|
value: [`small-${primaryKey}`],
|
|
id: primaryKey
|
|
});
|
|
break;
|
|
case 'get': // Tests returning a value.
|
|
case 'get-empty': // Tests returning undefined.
|
|
request = store.get(primaryKey);
|
|
break;
|
|
case 'getall': // Tests returning an array of values.
|
|
request = store.getAll();
|
|
break;
|
|
case 'error': // Tests returning an error.
|
|
request =
|
|
store.put({key: `k${primaryKey}`, value: [`small-${primaryKey}`]});
|
|
break;
|
|
case 'continue': // Tests returning a key, primary key, and value.
|
|
case 'continue-empty': // Tests returning null.
|
|
request = cursor;
|
|
cursor.result.continue();
|
|
break;
|
|
case 'open': // Tests returning a cursor, key, primary key, and value.
|
|
case 'open-empty': // Tests returning null.
|
|
request = index.openCursor(IDBKeyRange.lowerBound(`k${primaryKey}`));
|
|
break;
|
|
case 'count': // Tests returning a numeric result.
|
|
request = index.count();
|
|
break;
|
|
};
|
|
|
|
request.onsuccess = testCase.step_func(() => {
|
|
reject(new Error(
|
|
'requests should not succeed after the transaction is aborted'));
|
|
});
|
|
request.onerror = testCase.step_func(event => {
|
|
event.preventDefault();
|
|
results.push([requestId, request.error]);
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
function abortTest(label, operations) {
|
|
promise_test(testCase => {
|
|
return createDatabase(
|
|
testCase,
|
|
(database, transaction) => {
|
|
const store = database.createObjectStore(
|
|
'test-store', {autoIncrement: true, keyPath: 'id'});
|
|
store.createIndex('test-index', 'key', {unique: true});
|
|
populateStore(store);
|
|
})
|
|
.then(database => {
|
|
const transaction = database.transaction(['test-store'], 'readwrite');
|
|
const store = transaction.objectStore('test-store');
|
|
const index = store.index('test-index');
|
|
return new Promise((resolve, reject) => {
|
|
openCursors(testCase, index, operations, reject, () => {
|
|
const results = [];
|
|
const promises = [];
|
|
for (let i = 0; i < operations.length; ++i) {
|
|
const promise = doOperation(
|
|
testCase, store, index, operations[i], i, results);
|
|
promises.push(promise);
|
|
};
|
|
transaction.abort();
|
|
resolve(Promise.all(promises).then(() => results));
|
|
});
|
|
});
|
|
})
|
|
.then(results => {
|
|
assert_equals(
|
|
results.length, operations.length,
|
|
'Promise.all should resolve after all sub-promises resolve');
|
|
for (let i = 0; i < operations.length; ++i) {
|
|
assert_equals(
|
|
results[i][0], i,
|
|
'error event order should match request order');
|
|
assert_equals(
|
|
results[i][1].name, 'AbortError',
|
|
'transaction aborting should result in AbortError on all requests');
|
|
}
|
|
});
|
|
}, label);
|
|
}
|
|
|
|
abortTest('small values', [
|
|
['get', 2],
|
|
['count', null],
|
|
['continue-empty', null],
|
|
['get-empty', 5],
|
|
['add', 5],
|
|
['open', 2],
|
|
['continue', 2],
|
|
['get', 4],
|
|
['get-empty', 6],
|
|
['count', null],
|
|
['put-with-id', 5],
|
|
['put', 6],
|
|
['error', 3],
|
|
['continue', 4],
|
|
['count', null],
|
|
['get-empty', 7],
|
|
['open', 4],
|
|
['open-empty', 7],
|
|
['add', 7],
|
|
]);
|
|
|
|
abortTest('large values', [
|
|
['open', 1],
|
|
['get', 1],
|
|
['getall', 4],
|
|
['get', 3],
|
|
['continue', 3],
|
|
['open', 3],
|
|
]);
|
|
|
|
abortTest('large value followed by small values', [
|
|
['get', 1],
|
|
['getall', null],
|
|
['open', 2],
|
|
['continue-empty', null],
|
|
['get', 2],
|
|
['get-empty', 5],
|
|
['count', null],
|
|
['continue-empty', null],
|
|
['open-empty', 5],
|
|
['add', 5],
|
|
['error', 1],
|
|
['continue', 2],
|
|
['get-empty', 6],
|
|
['put-with-id', 5],
|
|
['put', 6],
|
|
]);
|
|
|
|
abortTest('large values mixed with small values', [
|
|
['get', 1],
|
|
['get', 2],
|
|
['get-empty', 5],
|
|
['count', null],
|
|
['continue-empty', null],
|
|
['open', 1],
|
|
['continue', 2],
|
|
['open-empty', 5],
|
|
['getall', 4],
|
|
['open', 2],
|
|
['continue-empty', null],
|
|
['add', 5],
|
|
['get', 3],
|
|
['count', null],
|
|
['get-empty', 6],
|
|
['put-with-id', 5],
|
|
['getall', null],
|
|
['continue', 3],
|
|
['open-empty', 6],
|
|
['put', 6],
|
|
['error', 1],
|
|
['continue', 2],
|
|
['open', 4],
|
|
['get-empty', 7],
|
|
['count', null],
|
|
['continue', 3],
|
|
['add', 7],
|
|
['getall', null],
|
|
['error', 3],
|
|
['count', null],
|
|
]);
|