Files
ladybird/Libraries/LibWeb/IndexedDB/IDBTransaction.h
Zaggy1024 3c24a394c6 LibWeb: Refactor IndexedDB to handle requests serially
Previously, after one request was marked as processed, we would
synchronously queue another task to process the next request. This
would mean that two open requests on the same database could
interleave. This was especially problematic when one of the requests
would cause the database to upgrade, since the second open request
would begin processing before the upgradeneeded event fired, causing an
exception to be thrown in the second open().

The solution is to explicitly check for continuation conditions after
events have been fired in order to ensure that every step for the
request is completed before starting any further request processing.

For connection requests, the spec states:

> Open requests are processed in a connection queue. The queue contains
> all open requests associated with an storage key and a name. Requests
> added to the connection queue processed in order and each request
> must run to completion before the next request is processed. An open
> request may be blocked on other connections, requiring those
> connections to close before the request can complete and allow
> further requests to be processed.

For requests against a transaction, the spec states:

> Once the transaction has been started the implementation can begin
> executing the requests placed against the transaction. Requests must
> be executed in the order in which they were made against the
> transaction. Likewise, their results must be returned in the order
> the requests were placed against a specific transaction. There is no
> guarantee about the order that results from requests in different
> transactions are returned.

In the process of reworking it to use this approach, I've added a bunch
of new tests that cover things that our imported WPTs weren't checking.

With the fix for serializing connection requests, we can now fully
download the assets for the emscripten-compiled asm.js games in the
Humble Mozilla Bundle, particularly FTL: Faster Than Light.

There were no regressions in our test suite. One web platform test,
'idbindex_reverse_cursor.any.html', has one newly-failing subtest, but
the subtest was apparently only passing by chance due synchronous
execution of requests. A few web platform tests that were added in a
prior commit improved. The delete-request-queue.any.html test has
stopped crashing, and the close-in-upgrade-needed.any.html test has
stopped flaking, so they are both imported here as well.

Incidentally fixes #7512, for which a crash test has been added.
2026-03-05 17:12:55 -06:00

122 lines
5.4 KiB
C++

/*
* Copyright (c) 2024-2025, stelar7 <dudedbz@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Vector.h>
#include <LibGC/Ptr.h>
#include <LibWeb/Bindings/IDBDatabasePrototype.h>
#include <LibWeb/Bindings/IDBTransactionPrototype.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/EventTarget.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/IndexedDB/IDBDatabase.h>
#include <LibWeb/IndexedDB/IDBRequest.h>
#include <LibWeb/IndexedDB/Internal/ObjectStore.h>
#include <LibWeb/IndexedDB/Internal/RequestList.h>
namespace Web::IndexedDB {
// https://w3c.github.io/IndexedDB/#transaction
class IDBTransaction : public DOM::EventTarget {
WEB_PLATFORM_OBJECT(IDBTransaction, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(IDBTransaction);
enum TransactionState {
Active,
Inactive,
Committing,
Finished
};
public:
virtual ~IDBTransaction() override;
[[nodiscard]] static GC::Ref<IDBTransaction> create(JS::Realm&, GC::Ref<IDBDatabase>, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector<GC::Ref<ObjectStore>>);
[[nodiscard]] Bindings::IDBTransactionMode mode() const { return m_mode; }
[[nodiscard]] TransactionState state() const { return m_state; }
[[nodiscard]] GC::Ptr<WebIDL::DOMException> error() const { return m_error; }
[[nodiscard]] GC::Ref<IDBDatabase> connection() const { return m_connection; }
[[nodiscard]] Bindings::IDBTransactionDurability durability() const { return m_durability; }
[[nodiscard]] GC::Ptr<IDBRequest> associated_request() const { return m_associated_request; }
[[nodiscard]] bool aborted() const { return m_aborted; }
[[nodiscard]] GC::Ref<HTML::DOMStringList> object_store_names();
[[nodiscard]] RequestList& request_list() { return m_request_list; }
[[nodiscard]] ReadonlySpan<GC::Ref<ObjectStore>> scope() const { return m_scope; }
[[nodiscard]] String uuid() const { return m_uuid; }
[[nodiscard]] GC::Ptr<HTML::EventLoop> cleanup_event_loop() const { return m_cleanup_event_loop; }
void set_mode(Bindings::IDBTransactionMode mode) { m_mode = mode; }
void set_error(GC::Ptr<WebIDL::DOMException> error) { m_error = error; }
void set_associated_request(GC::Ptr<IDBRequest> request) { m_associated_request = request; }
void set_aborted(bool aborted) { m_aborted = aborted; }
void set_cleanup_event_loop(GC::Ptr<HTML::EventLoop> event_loop) { m_cleanup_event_loop = event_loop; }
void set_state(TransactionState state);
[[nodiscard]] bool is_upgrade_transaction() const { return m_mode == Bindings::IDBTransactionMode::Versionchange; }
[[nodiscard]] bool is_readonly() const { return m_mode == Bindings::IDBTransactionMode::Readonly; }
[[nodiscard]] bool is_readwrite() const { return m_mode == Bindings::IDBTransactionMode::Readwrite; }
[[nodiscard]] bool is_finished() const { return m_state == TransactionState::Finished; }
[[nodiscard]] bool is_active() const { return m_state == TransactionState::Active; }
[[nodiscard]] bool is_inactive() const { return m_state == TransactionState::Inactive; }
[[nodiscard]] bool is_committing() const { return m_state == TransactionState::Committing; }
GC::Ptr<ObjectStore> object_store_named(String const& name) const;
void add_to_scope(GC::Ref<ObjectStore> object_store) { m_scope.append(object_store); }
WebIDL::ExceptionOr<void> abort();
WebIDL::ExceptionOr<void> commit();
WebIDL::ExceptionOr<GC::Ref<IDBObjectStore>> object_store(String const& name);
void set_onabort(WebIDL::CallbackType*);
WebIDL::CallbackType* onabort();
void set_oncomplete(WebIDL::CallbackType*);
WebIDL::CallbackType* oncomplete();
void set_onerror(WebIDL::CallbackType*);
WebIDL::CallbackType* onerror();
protected:
explicit IDBTransaction(JS::Realm&, GC::Ref<IDBDatabase>, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector<GC::Ref<ObjectStore>>);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Visitor& visitor) override;
private:
// AD-HOC: The transaction has a connection
GC::Ref<IDBDatabase> m_connection;
// A transaction has a mode that determines which types of interactions can be performed upon that transaction.
Bindings::IDBTransactionMode m_mode;
// A transaction has a durability hint. This is a hint to the user agent of whether to prioritize performance or durability when committing the transaction.
Bindings::IDBTransactionDurability m_durability { Bindings::IDBTransactionDurability::Default };
// A transaction has a state
TransactionState m_state { TransactionState::Active };
// A transaction has a error which is set if the transaction is aborted.
GC::Ptr<WebIDL::DOMException> m_error;
// A transaction has an associated upgrade request
GC::Ptr<IDBRequest> m_associated_request;
// AD-HOC: We need to track abort state separately, since we cannot rely on only the error.
bool m_aborted { false };
// A transaction has a scope which is a set of object stores that the transaction may interact with.
Vector<GC::Ref<ObjectStore>> m_scope;
// A transaction has a request list of pending requests which have been made against the transaction.
RequestList m_request_list;
// A transaction optionally has a cleanup event loop which is an event loop.
GC::Ptr<HTML::EventLoop> m_cleanup_event_loop;
// NOTE: Used for debug purposes
String m_uuid;
};
}