LibWeb: Fire the "blocked" event at the IDB open request

...rather than at each of the open connections. These disagreed with the
spec steps written right above them.

The transaction-lifetime.any.html and idbdatabase_close.any.html tests
pass instead of timing out with these changes, so they've been imported.
This commit is contained in:
Zaggy1024 2026-03-05 05:39:04 -06:00 committed by Gregory Bertilson
parent 5f114d730b
commit 5eeac1b9dd
7 changed files with 224 additions and 13 deletions

View file

@ -159,12 +159,17 @@ void open_a_database_connection(JS::Realm& realm, StorageAPI::StorageKey storage
auto after_all = GC::create_function(realm.heap(), [&realm, &queue, open_connections, db, version, connection, request, on_complete] {
// 4. If any of the connections in openConnections are still not closed,
// queue a database task to fire a version change event named blocked at request with dbs version and version.
for (auto const& entry : open_connections->elements()) {
if (entry->state() != ConnectionState::Closed) {
queue_a_database_task(GC::create_function(realm.vm().heap(), [&realm, entry, db, version]() {
fire_a_version_change_event(realm, HTML::EventNames::blocked, *entry, db->version(), version);
}));
auto any_connection_is_not_closed = [&] {
for (auto const& entry : open_connections->elements()) {
if (entry->state() != ConnectionState::Closed)
return true;
}
return false;
}();
if (any_connection_is_not_closed) {
queue_a_database_task(GC::create_function(realm.vm().heap(), [&realm, request, db, version]() {
fire_a_version_change_event(realm, HTML::EventNames::blocked, request, db->version(), version);
}));
}
// 5. Wait until all connections in openConnections are closed.
@ -482,7 +487,7 @@ void delete_a_database(JS::Realm& realm, StorageAPI::StorageKey storage_key, Str
// 2. Add request to queue.
// 3. Wait until all previous requests in queue have been processed.
queue.enqueue(request, GC::create_function(realm.heap(), [&realm, &queue, storage_key = move(storage_key), name = move(name), on_complete] -> void {
queue.enqueue(request, GC::create_function(realm.heap(), [&realm, &queue, storage_key = move(storage_key), name = move(name), request, on_complete] -> void {
static constexpr auto call_completion = [](auto& queue, auto completion, auto result) {
completion->function()(move(result));
queue.on_request_processed();
@ -523,14 +528,20 @@ void delete_a_database(JS::Realm& realm, StorageAPI::StorageKey storage_key, Str
dbgln("remaining tasks: {}", task_counter_state ? task_counter_state->remaining_tasks : 0);
}
auto after_all = GC::create_function(realm.heap(), [&realm, &queue, open_connections, db, storage_key, name, on_complete] {
// 8. If any of the connections in openConnections are still not closed, queue a database task to fire a version change event named blocked at request with dbs version and null.
for (auto const& entry : open_connections->elements()) {
if (entry->state() != ConnectionState::Closed) {
queue_a_database_task(GC::create_function(realm.vm().heap(), [&realm, entry, db]() {
fire_a_version_change_event(realm, HTML::EventNames::blocked, *entry, db->version(), {});
}));
auto after_all = GC::create_function(realm.heap(), [&realm, &queue, open_connections, db, storage_key, name, request, on_complete] {
// 8. If any of the connections in openConnections are still not closed, queue a database task to fire a
// version change event named blocked at request with dbs version and null.
auto any_connection_is_not_closed = [&] {
for (auto const& entry : open_connections->elements()) {
if (entry->state() != ConnectionState::Closed)
return true;
}
return false;
}();
if (any_connection_is_not_closed) {
queue_a_database_task(GC::create_function(realm.vm().heap(), [&realm, request, db]() {
fire_a_version_change_event(realm, HTML::EventNames::blocked, request, db->version(), {});
}));
}
// 9. Wait until all connections in openConnections are closed.

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass Unblock the version change transaction created by an open database request
Pass Unblock the delete database request.

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass No Blocked event
Pass Blocked event

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>IDBDatabase.close()</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="resources/support.js"></script>
<div id=log></div>
<script src="../IndexedDB/idbdatabase_close.any.js"></script>

View file

@ -0,0 +1,74 @@
// META: global=window,worker
// META: title=IDBDatabase.close()
// META: script=resources/support.js
// Spec: https://w3c.github.io/IndexedDB/#dom-idbdatabase-transaction
'use strict';
async_test(t => {
let db;
let versionchange_fired;
let blocked_fired;
let upgradeneeded_fired;
const open_rq = createdb(t);
let counter = 0;
open_rq.onupgradeneeded = function() {};
open_rq.onsuccess = function(e) {
db = e.target.result;
db.onversionchange = t.step_func((e) => {
versionchange_fired = counter++;
});
const rq = indexedDB.open(db.name, db.version + 1);
rq.onblocked = t.step_func((e) => {
blocked_fired = counter++;
db.close();
});
rq.onupgradeneeded = t.step_func((e) => {
upgradeneeded_fired = counter++;
});
rq.onsuccess = t.step_func((e) => {
assert_equals(versionchange_fired, 0, 'versionchange event fired #');
assert_equals(blocked_fired, 1, 'block event fired #');
assert_equals(
upgradeneeded_fired, 2, 'second upgradeneeded event fired #');
rq.result.close();
t.done();
});
rq.onerror = t.step_func(e => {
assert_unreached('Unexpected database deletion error: ' + e.target.error);
});
};
}, 'Unblock the version change transaction created by an open database request');
async_test(t => {
let db;
let blocked_fired = false;
let versionchange_fired = false;
const open_rq = createdb(t);
open_rq.onupgradeneeded = t.step_func(e => {});
open_rq.onsuccess = t.step_func(e => {
db = e.target.result;
db.onversionchange = t.step_func(e => {
versionchange_fired = true;
});
const rq = indexedDB.deleteDatabase(db.name);
rq.onblocked = t.step_func(e => {
blocked_fired = true;
db.close();
});
rq.onsuccess = t.step_func(e => {
assert_true(versionchange_fired, 'versionchange event fired')
assert_true(blocked_fired, 'block event fired')
t.done();
});
rq.onerror = t.step_func(e => {
assert_unreached('Unexpected database deletion error: ' + e.target.error);
});
});
}, 'Unblock the delete database request.');

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Event order when opening a second database when one connection is open already</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="resources/support.js"></script>
<div id=log></div>
<script src="../IndexedDB/transaction-lifetime.any.js"></script>

View file

@ -0,0 +1,82 @@
// META: title=Event order when opening a second database when one connection is open already
// META: global=window,worker
// META: script=resources/support.js
'use strict';
async function setupDatabase(t, dbname, version) {
indexedDB.deleteDatabase(dbname);
const openrq = indexedDB.open(dbname, version);
const eventWatcher = new EventWatcher(
t, openrq, ['upgradeneeded', 'error', 'blocked', 'success']);
let event = await eventWatcher.wait_for('upgradeneeded');
const db = event.target.result;
db.createObjectStore('store');
await eventWatcher.wait_for('success');
return db;
}
promise_test(async t => {
const dbname = location + '-' + t.name;
const version = 3;
const db = await setupDatabase(t, dbname, version);
let db2;
t.add_cleanup(() => {
if (db2)
db2.close();
if (db)
db.close();
indexedDB.deleteDatabase(dbname);
});
const dbWatcher = new EventWatcher(t, db, ['versionchange', 'close']);
const openrq2 = indexedDB.open(dbname, version + 1);
let versionChangeEvent = await dbWatcher.wait_for('versionchange');
const openrq2Watcher = new EventWatcher(
t, openrq2, ['upgradeneeded', 'success', 'error', 'blocked']);
assert_equals(versionChangeEvent.oldVersion, version, 'old version');
assert_equals(versionChangeEvent.newVersion, version + 1, 'new version');
db.close();
await openrq2Watcher.wait_for('upgradeneeded');
let successEvent = await openrq2Watcher.wait_for('success');
db2 = successEvent.target.result;
}, 'No Blocked event');
promise_test(async t => {
const dbname = location + '-' + t.name;
const version = 3;
const db = await setupDatabase(t, dbname, version);
let db2;
t.add_cleanup(() => {
if (db2)
db2.close();
if (db)
db.close();
indexedDB.deleteDatabase(dbname);
});
const dbWatcher = new EventWatcher(t, db, ['versionchange', 'close']);
const openrq2 = indexedDB.open(dbname, version + 1);
let versionChangeEvent = await dbWatcher.wait_for('versionchange');
const openrq2Watcher = new EventWatcher(
t, openrq2, ['blocked', 'upgradeneeded', 'error', 'success']);
assert_equals(versionChangeEvent.oldVersion, version, 'old version');
assert_equals(versionChangeEvent.newVersion, version + 1, 'new version');
let blockedEvent = await openrq2Watcher.wait_for('blocked');
db.close();
await openrq2Watcher.wait_for('upgradeneeded');
let successEvent = await openrq2Watcher.wait_for('success');
db2 = successEvent.target.result;
}, 'Blocked event');