Skip to content

Commit d87c2a5

Browse files
Lubrsiawesomekling
authored andcommitted
LibWeb/IndexedDB: Remove spin_until from checking finished transactions
1 parent 52b53e5 commit d87c2a5

File tree

10 files changed

+251
-43
lines changed

10 files changed

+251
-43
lines changed

Libraries/LibWeb/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@ set(SOURCES
684684
IndexedDB/Internal/Database.cpp
685685
IndexedDB/Internal/IDBDatabaseObserver.cpp
686686
IndexedDB/Internal/IDBRequestObserver.cpp
687+
IndexedDB/Internal/IDBTransactionObserver.cpp
687688
IndexedDB/Internal/Index.cpp
688689
IndexedDB/Internal/Key.cpp
689690
IndexedDB/Internal/ObjectStore.cpp

Libraries/LibWeb/Forward.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,7 @@ class IDBRecord;
813813
class IDBRequest;
814814
class IDBRequestObserver;
815815
class IDBTransaction;
816+
class IDBTransactionObserver;
816817
class IDBVersionChangeEvent;
817818
class Index;
818819
class ObjectStore;

Libraries/LibWeb/IndexedDB/IDBDatabase.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
#include <LibWeb/IndexedDB/IDBObjectStore.h>
1313
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
1414
#include <LibWeb/IndexedDB/Internal/IDBDatabaseObserver.h>
15+
#include <LibWeb/IndexedDB/Internal/IDBTransactionObserver.h>
1516

1617
namespace Web::IndexedDB {
1718

1819
GC_DEFINE_ALLOCATOR(IDBDatabase);
20+
GC_DEFINE_ALLOCATOR(IDBDatabase::TransactionFinishState);
1921

2022
IDBDatabase::IDBDatabase(JS::Realm& realm, Database& db)
2123
: EventTarget(realm)
@@ -47,6 +49,7 @@ void IDBDatabase::visit_edges(Visitor& visitor)
4749
visitor.visit(m_object_store_set);
4850
visitor.visit(m_associated_database);
4951
visitor.visit(m_transactions);
52+
visitor.visit(m_transaction_finish_queue);
5053
}
5154

5255
void IDBDatabase::set_onabort(WebIDL::CallbackType* event_handler)
@@ -263,4 +266,60 @@ void IDBDatabase::set_state(ConnectionState state)
263266
});
264267
}
265268

269+
void IDBDatabase::wait_for_transactions_to_finish(ReadonlySpan<GC::Ref<IDBTransaction>> transactions, GC::Ref<GC::Function<void()>> on_complete)
270+
{
271+
GC::Ptr<TransactionFinishState> transaction_finish_state;
272+
273+
for (auto const& entry : transactions) {
274+
if (!entry->is_finished()) {
275+
if (!transaction_finish_state) {
276+
transaction_finish_state = heap().allocate<TransactionFinishState>();
277+
}
278+
279+
transaction_finish_state->add_transaction_to_observe(entry);
280+
}
281+
}
282+
283+
if (transaction_finish_state) {
284+
transaction_finish_state->after_all = GC::create_function(heap(), [this, transaction_finish_state, on_complete] {
285+
bool was_removed = m_transaction_finish_queue.remove_first_matching([transaction_finish_state](GC::Ref<TransactionFinishState> pending_transaction_finish_state) {
286+
return pending_transaction_finish_state == transaction_finish_state;
287+
});
288+
VERIFY(was_removed);
289+
queue_a_database_task(on_complete);
290+
});
291+
m_transaction_finish_queue.append(transaction_finish_state.as_nonnull());
292+
} else {
293+
queue_a_database_task(on_complete);
294+
}
295+
}
296+
297+
void IDBDatabase::TransactionFinishState::visit_edges(Visitor& visitor)
298+
{
299+
Base::visit_edges(visitor);
300+
visitor.visit(transaction_observers);
301+
visitor.visit(after_all);
302+
}
303+
304+
void IDBDatabase::TransactionFinishState::add_transaction_to_observe(GC::Ref<IDBTransaction> transaction)
305+
{
306+
auto transaction_observer = heap().allocate<IDBTransactionObserver>(transaction);
307+
transaction_observer->set_transaction_finished_observer(GC::create_function(heap(), [this] {
308+
transaction_observers.remove_all_matching([](GC::Ref<IDBTransactionObserver> const& transaction_observer) {
309+
if (transaction_observer->transaction()->is_finished()) {
310+
transaction_observer->unobserve();
311+
return true;
312+
}
313+
314+
return false;
315+
});
316+
317+
if (transaction_observers.is_empty()) {
318+
queue_a_database_task(after_all.as_nonnull());
319+
}
320+
}));
321+
322+
transaction_observers.append(transaction_observer);
323+
}
324+
266325
}

Libraries/LibWeb/IndexedDB/IDBDatabase.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class IDBDatabase : public DOM::EventTarget {
8383
void register_database_observer(Badge<IDBDatabaseObserver>, IDBDatabaseObserver&);
8484
void unregister_database_observer(Badge<IDBDatabaseObserver>, IDBDatabaseObserver&);
8585

86+
void wait_for_transactions_to_finish(ReadonlySpan<GC::Ref<IDBTransaction>>, GC::Ref<GC::Function<void()>> on_complete);
87+
8688
protected:
8789
explicit IDBDatabase(JS::Realm&, Database&);
8890

@@ -110,6 +112,20 @@ class IDBDatabase : public DOM::EventTarget {
110112
HashTable<GC::RawRef<IDBDatabaseObserver>> m_database_observers;
111113
Vector<GC::Ref<IDBDatabaseObserver>> m_database_observers_being_notified;
112114

115+
struct TransactionFinishState final : public GC::Cell {
116+
GC_CELL(TransactionFinishState, GC::Cell);
117+
GC_DECLARE_ALLOCATOR(TransactionFinishState);
118+
119+
virtual void visit_edges(Visitor& visitor) override;
120+
121+
void add_transaction_to_observe(GC::Ref<IDBTransaction> transaction);
122+
123+
Vector<GC::Ref<IDBTransactionObserver>> transaction_observers;
124+
GC::Ptr<GC::Function<void()>> after_all;
125+
};
126+
127+
Vector<GC::Ref<TransactionFinishState>> m_transaction_finish_queue;
128+
113129
u64 m_version { 0 };
114130
String m_name;
115131

Libraries/LibWeb/IndexedDB/IDBTransaction.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <LibWeb/IndexedDB/IDBObjectStore.h>
1212
#include <LibWeb/IndexedDB/IDBTransaction.h>
1313
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
14+
#include <LibWeb/IndexedDB/Internal/IDBTransactionObserver.h>
1415

1516
namespace Web::IndexedDB {
1617

@@ -43,6 +44,7 @@ void IDBTransaction::initialize(JS::Realm& realm)
4344
void IDBTransaction::visit_edges(Visitor& visitor)
4445
{
4546
Base::visit_edges(visitor);
47+
visitor.visit(m_transaction_observers_being_notified);
4648
visitor.visit(m_connection);
4749
visitor.visit(m_error);
4850
visitor.visit(m_associated_request);
@@ -147,4 +149,27 @@ WebIDL::ExceptionOr<GC::Ref<IDBObjectStore>> IDBTransaction::object_store(String
147149
return IDBObjectStore::create(realm, *store, *this);
148150
}
149151

152+
void IDBTransaction::register_transaction_observer(Badge<IDBTransactionObserver>, IDBTransactionObserver& database_observer)
153+
{
154+
auto result = m_transaction_observers.set(database_observer);
155+
VERIFY(result == AK::HashSetResult::InsertedNewEntry);
156+
}
157+
158+
void IDBTransaction::unregister_transaction_observer(Badge<IDBTransactionObserver>, IDBTransactionObserver& database_observer)
159+
{
160+
bool was_removed = m_transaction_observers.remove(database_observer);
161+
VERIFY(was_removed);
162+
}
163+
164+
void IDBTransaction::set_state(TransactionState state)
165+
{
166+
m_state = state;
167+
168+
if (m_state == TransactionState::Finished) {
169+
notify_each_transaction_observer([](IDBTransactionObserver const& transaction_observer) {
170+
return transaction_observer.transaction_finished_observer();
171+
});
172+
}
173+
}
174+
150175
}

Libraries/LibWeb/IndexedDB/IDBTransaction.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ class IDBTransaction : public DOM::EventTarget {
5454
void set_associated_request(GC::Ptr<IDBRequest> request) { m_associated_request = request; }
5555
void set_aborted(bool aborted) { m_aborted = aborted; }
5656
void set_cleanup_event_loop(GC::Ptr<HTML::EventLoop> event_loop) { m_cleanup_event_loop = event_loop; }
57-
void set_state(TransactionState state) { m_state = state; }
57+
void set_state(TransactionState state);
5858

5959
[[nodiscard]] bool is_upgrade_transaction() const { return m_mode == Bindings::IDBTransactionMode::Versionchange; }
6060
[[nodiscard]] bool is_readonly() const { return m_mode == Bindings::IDBTransactionMode::Readonly; }
@@ -78,12 +78,35 @@ class IDBTransaction : public DOM::EventTarget {
7878
void set_onerror(WebIDL::CallbackType*);
7979
WebIDL::CallbackType* onerror();
8080

81+
void register_transaction_observer(Badge<IDBTransactionObserver>, IDBTransactionObserver&);
82+
void unregister_transaction_observer(Badge<IDBTransactionObserver>, IDBTransactionObserver&);
83+
8184
protected:
8285
explicit IDBTransaction(JS::Realm&, GC::Ref<IDBDatabase>, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector<GC::Ref<ObjectStore>>);
8386
virtual void initialize(JS::Realm&) override;
8487
virtual void visit_edges(Visitor& visitor) override;
8588

8689
private:
90+
template<typename GetNotifier, typename... Args>
91+
void notify_each_transaction_observer(GetNotifier&& get_notifier, Args&&... args)
92+
{
93+
ScopeGuard guard { [&]() { m_transaction_observers_being_notified.clear_with_capacity(); } };
94+
m_transaction_observers_being_notified.ensure_capacity(m_transaction_observers.size());
95+
96+
for (auto observer : m_transaction_observers)
97+
m_transaction_observers_being_notified.unchecked_append(observer);
98+
99+
for (auto transaction_observer : m_transaction_observers) {
100+
if (auto notifier = get_notifier(*transaction_observer))
101+
notifier->function()(forward<Args>(args)...);
102+
}
103+
}
104+
105+
// IDBTransaction should not visit IDBTransactionObserver to avoid leaks.
106+
// It's responsibility of object that requires IDBTransactionObserver to keep it alive.
107+
HashTable<GC::RawRef<IDBTransactionObserver>> m_transaction_observers;
108+
Vector<GC::Ref<IDBTransactionObserver>> m_transaction_observers_being_notified;
109+
87110
// AD-HOC: The transaction has a connection
88111
GC::Ref<IDBDatabase> m_connection;
89112

Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp

Lines changed: 37 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -182,24 +182,25 @@ void open_a_database_connection(JS::Realm& realm, StorageAPI::StorageKey storage
182182

183183
db->wait_for_connections_to_close(open_connections, GC::create_function(realm.heap(), [&realm, connection, version, request, on_complete] {
184184
// 6. Run upgrade a database using connection, version and request.
185-
upgrade_a_database(realm, connection, version, request);
186-
187-
// 7. If connection was closed, return a newly created "AbortError" DOMException and abort these steps.
188-
if (connection->state() == ConnectionState::Closed) {
189-
on_complete->function()(WebIDL::AbortError::create(realm, "Connection was closed"_utf16));
190-
return;
191-
}
192-
193-
// 8. If request's error is set, run the steps to close a database connection with connection,
194-
// return a newly created "AbortError" DOMException and abort these steps.
195-
if (request->has_error()) {
196-
close_a_database_connection(*connection);
197-
on_complete->function()(WebIDL::AbortError::create(realm, "Upgrade transaction was aborted"_utf16));
198-
return;
199-
}
200-
201-
// 11. Return connection.
202-
on_complete->function()(connection);
185+
upgrade_a_database(realm, connection, version, request, GC::create_function(realm.heap(), [&realm, connection, request, on_complete] {
186+
// 7. If connection was closed, return a newly created "AbortError" DOMException and abort these steps.
187+
if (connection->state() == ConnectionState::Closed) {
188+
on_complete->function()(WebIDL::AbortError::create(realm, "Connection was closed"_utf16));
189+
return;
190+
}
191+
192+
// 8. If request's error is set, run the steps to close a database connection with connection,
193+
// return a newly created "AbortError" DOMException and abort these steps.
194+
if (request->has_error()) {
195+
close_a_database_connection(*connection, GC::create_function(realm.heap(), [&realm, on_complete] {
196+
on_complete->function()(WebIDL::AbortError::create(realm, "Upgrade transaction was aborted"_utf16));
197+
}));
198+
return;
199+
}
200+
201+
// 11. Return connection.
202+
on_complete->function()(connection);
203+
}));
203204
}));
204205
});
205206

@@ -356,7 +357,7 @@ WebIDL::ExceptionOr<GC::Ref<Key>> convert_a_value_to_a_key(JS::Realm& realm, JS:
356357
}
357358

358359
// https://w3c.github.io/IndexedDB/#close-a-database-connection
359-
void close_a_database_connection(GC::Ref<IDBDatabase> connection, bool forced)
360+
void close_a_database_connection(GC::Ref<IDBDatabase> connection, GC::Ptr<GC::Function<void()>> on_complete, bool forced)
360361
{
361362
auto& realm = connection->realm();
362363

@@ -371,32 +372,28 @@ void close_a_database_connection(GC::Ref<IDBDatabase> connection, bool forced)
371372
}
372373

373374
// 3. Wait for all transactions created using connection to complete. Once they are complete, connection is closed.
374-
HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [connection]() {
375-
if constexpr (IDB_DEBUG) {
376-
dbgln("close_a_database_connection: waiting for step 3");
377-
dbgln("transactions created using connection:");
378-
for (auto const& transaction : connection->transactions()) {
379-
dbgln(" - {} - {}", transaction->uuid(), (u8)transaction->state());
380-
}
381-
}
382-
375+
if constexpr (IDB_DEBUG) {
376+
dbgln("close_a_database_connection: waiting for step 3");
377+
dbgln("transactions created using connection:");
383378
for (auto const& transaction : connection->transactions()) {
384-
if (!transaction->is_finished())
385-
return false;
379+
dbgln(" - {} - {}", transaction->uuid(), (u8)transaction->state());
386380
}
381+
}
387382

388-
return true;
389-
}));
383+
connection->wait_for_transactions_to_finish(connection->transactions(), GC::create_function(realm.heap(), [&realm, connection, forced, on_complete] {
384+
connection->set_state(ConnectionState::Closed);
390385

391-
connection->set_state(ConnectionState::Closed);
386+
// 4. If the forced flag is true, then fire an event named close at connection.
387+
if (forced)
388+
connection->dispatch_event(DOM::Event::create(realm, HTML::EventNames::close));
392389

393-
// 4. If the forced flag is true, then fire an event named close at connection.
394-
if (forced)
395-
connection->dispatch_event(DOM::Event::create(realm, HTML::EventNames::close));
390+
if (on_complete)
391+
queue_a_database_task(on_complete.as_nonnull());
392+
}));
396393
}
397394

398395
// https://w3c.github.io/IndexedDB/#upgrade-a-database
399-
void upgrade_a_database(JS::Realm& realm, GC::Ref<IDBDatabase> connection, u64 version, GC::Ref<IDBRequest> request)
396+
void upgrade_a_database(JS::Realm& realm, GC::Ref<IDBDatabase> connection, u64 version, GC::Ref<IDBRequest> request, GC::Ref<GC::Function<void()>> on_complete)
400397
{
401398
// 1. Let db be connection’s database.
402399
auto db = connection->associated_database();
@@ -463,9 +460,9 @@ void upgrade_a_database(JS::Realm& realm, GC::Ref<IDBDatabase> connection, u64 v
463460
}));
464461

465462
// 11. Wait for transaction to finish.
466-
HTML::main_thread_event_loop().spin_until(GC::create_function(realm.vm().heap(), [transaction]() {
467-
dbgln_if(IDB_DEBUG, "upgrade_a_database: waiting for step 11");
468-
return transaction->is_finished();
463+
dbgln_if(IDB_DEBUG, "upgrade_a_database: waiting for step 11");
464+
connection->wait_for_transactions_to_finish({ &transaction, 1 }, GC::create_function(realm.heap(), [on_complete] {
465+
queue_a_database_task(on_complete);
469466
}));
470467
}
471468

Libraries/LibWeb/IndexedDB/Internal/Algorithms.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ using RecordSource = Variant<GC::Ref<ObjectStore>, GC::Ref<Index>>;
3030
void open_a_database_connection(JS::Realm&, StorageAPI::StorageKey, String, Optional<u64>, GC::Ref<IDBRequest>, GC::Ref<GC::Function<void(WebIDL::ExceptionOr<GC::Ref<IDBDatabase>>)>>);
3131
bool fire_a_version_change_event(JS::Realm&, FlyString const&, GC::Ref<DOM::EventTarget>, u64, Optional<u64>);
3232
WebIDL::ExceptionOr<GC::Ref<Key>> convert_a_value_to_a_key(JS::Realm&, JS::Value, Vector<JS::Value> = {});
33-
void close_a_database_connection(GC::Ref<IDBDatabase>, bool forced = false);
34-
void upgrade_a_database(JS::Realm&, GC::Ref<IDBDatabase>, u64, GC::Ref<IDBRequest>);
33+
void close_a_database_connection(GC::Ref<IDBDatabase>, GC::Ptr<GC::Function<void()>> on_complete = nullptr, bool forced = false);
34+
void upgrade_a_database(JS::Realm&, GC::Ref<IDBDatabase>, u64, GC::Ref<IDBRequest>, GC::Ref<GC::Function<void()>>);
3535
void delete_a_database(JS::Realm&, StorageAPI::StorageKey, String, GC::Ref<IDBRequest>, GC::Ref<GC::Function<void(WebIDL::ExceptionOr<u64>)>>);
3636
void abort_a_transaction(GC::Ref<IDBTransaction>, GC::Ptr<WebIDL::DOMException>);
3737
JS::Value convert_a_key_to_a_value(JS::Realm&, GC::Ref<Key>);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
3+
*
4+
* SPDX-License-Identifier: BSD-2-Clause
5+
*/
6+
7+
#include <LibWeb/IndexedDB/IDBTransaction.h>
8+
#include <LibWeb/IndexedDB/Internal/IDBTransactionObserver.h>
9+
10+
namespace Web::IndexedDB {
11+
12+
GC_DEFINE_ALLOCATOR(IDBTransactionObserver);
13+
14+
IDBTransactionObserver::IDBTransactionObserver(IDBTransaction& transaction)
15+
: m_transaction(transaction)
16+
{
17+
m_transaction->register_transaction_observer({}, *this);
18+
m_observing = true;
19+
}
20+
21+
IDBTransactionObserver::~IDBTransactionObserver() = default;
22+
23+
void IDBTransactionObserver::visit_edges(Cell::Visitor& visitor)
24+
{
25+
Base::visit_edges(visitor);
26+
visitor.visit(m_transaction);
27+
visitor.visit(m_transaction_finished_observer);
28+
}
29+
30+
void IDBTransactionObserver::finalize()
31+
{
32+
Base::finalize();
33+
unobserve();
34+
}
35+
36+
void IDBTransactionObserver::unobserve()
37+
{
38+
if (!m_observing)
39+
return;
40+
41+
m_transaction->unregister_transaction_observer({}, *this);
42+
m_observing = false;
43+
}
44+
45+
}

0 commit comments

Comments
 (0)