LibWeb: Open waiting IDB connections when the previous one is GCed

Without this, an open request could hang if a prior connection was not
explicitly close()d but instead allowed to go out of scope.

The spec says that the dangling connection should be closed when the
execution context it was opened in is destroyed, but no other browser
does so. Detecting the execution context being destroyed would likely
mean a lot of overhead in JS calls, so it's best to avoid that, despite
this being observable.
This commit is contained in:
Zaggy1024 2026-04-03 23:31:19 -05:00 committed by Alexander Kalenik
parent ed8ba673de
commit 61b9be47ce
4 changed files with 62 additions and 0 deletions

View file

@ -29,6 +29,18 @@ IDBDatabase::IDBDatabase(JS::Realm& realm, Database& db)
IDBDatabase::~IDBDatabase() = default;
void IDBDatabase::finalize()
{
Base::finalize();
m_associated_database->dissociate(*this);
heap().enqueue_post_gc_task([database = GC::Weak(m_associated_database)] {
if (!database)
return;
database->check_pending_connection_wait();
});
}
GC::Ref<IDBDatabase> IDBDatabase::create(JS::Realm& realm, Database& db)
{
return realm.create<IDBDatabase>(realm, db);

View file

@ -40,7 +40,10 @@ class IDBDatabase : public DOM::EventTarget {
GC_DECLARE_ALLOCATOR(IDBDatabase);
public:
static constexpr bool OVERRIDES_FINALIZE = true;
virtual ~IDBDatabase() override;
virtual void finalize() override;
[[nodiscard]] static GC::Ref<IDBDatabase> create(JS::Realm&, Database&);

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<script src="include.js"></script>
<script>
asyncTest(done => {
const DB_NAME = "gc-closes-unreachable-connection";
indexedDB.deleteDatabase(DB_NAME);
function openAndForget() {
return new Promise((resolve) => {
let req = indexedDB.open(DB_NAME, 1);
req.onupgradeneeded = (e) => {
e.target.result.createObjectStore("store");
};
req.onsuccess = (e) => {
resolve();
};
});
}
function tryUpgrade() {
let req = indexedDB.open(DB_NAME, 2);
req.onblocked = () => {
println("FAIL: upgrade blocked, connection was not closed by GC");
done();
};
req.onupgradeneeded = () => {
println("PASS");
};
req.onsuccess = () => {
req.result.close();
indexedDB.deleteDatabase(DB_NAME);
done();
};
}
openAndForget().then(() => {
return internals.gcAsync()
}).then(() => {
tryUpgrade();
});
});
</script>