mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 01:35:08 +02:00
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.
238 lines
10 KiB
C++
238 lines
10 KiB
C++
/*
|
||
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
|
||
* Copyright (c) 2024-2025, stelar7 <dudedbz@gmail.com>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <AK/Vector.h>
|
||
#include <LibJS/Runtime/Array.h>
|
||
#include <LibJS/Runtime/Value.h>
|
||
#include <LibWeb/Bindings/IDBFactoryPrototype.h>
|
||
#include <LibWeb/Bindings/Intrinsics.h>
|
||
#include <LibWeb/DOM/Event.h>
|
||
#include <LibWeb/HTML/EventNames.h>
|
||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||
#include <LibWeb/IndexedDB/IDBDatabase.h>
|
||
#include <LibWeb/IndexedDB/IDBFactory.h>
|
||
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
|
||
#include <LibWeb/IndexedDB/Internal/Key.h>
|
||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||
#include <LibWeb/StorageAPI/StorageKey.h>
|
||
#include <LibWeb/WebIDL/Promise.h>
|
||
|
||
namespace Web::IndexedDB {
|
||
|
||
GC_DEFINE_ALLOCATOR(IDBFactory);
|
||
|
||
IDBFactory::IDBFactory(JS::Realm& realm)
|
||
: Bindings::PlatformObject(realm)
|
||
{
|
||
}
|
||
|
||
IDBFactory::~IDBFactory() = default;
|
||
|
||
void IDBFactory::initialize(JS::Realm& realm)
|
||
{
|
||
WEB_SET_PROTOTYPE_FOR_INTERFACE(IDBFactory);
|
||
Base::initialize(realm);
|
||
}
|
||
|
||
// https://w3c.github.io/IndexedDB/#dom-idbfactory-open
|
||
WebIDL::ExceptionOr<GC::Ref<IDBOpenDBRequest>> IDBFactory::open(String const& name, Optional<u64> version)
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. If version is 0 (zero), throw a TypeError.
|
||
if (version.has_value() && version.value() == 0)
|
||
return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "The version provided must not be 0"_string };
|
||
|
||
// 2. Let environment be this's relevant settings object.
|
||
auto& environment = HTML::relevant_settings_object(*this);
|
||
|
||
// 3. Let storageKey be the result of running obtain a storage key given environment.
|
||
// If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
|
||
auto storage_key = StorageAPI::obtain_a_storage_key(environment);
|
||
if (!storage_key.has_value())
|
||
return WebIDL::SecurityError::create(realm, "Failed to obtain a storage key"_utf16);
|
||
|
||
// 4. Let request be a new open request.
|
||
auto request = IDBOpenDBRequest::create(realm);
|
||
|
||
// 5. Run these steps in parallel:
|
||
// NB: We could defer these steps, but it wouldn't be observable anyway, since open_a_database_connection() will
|
||
// put this work into a queue and then process it later in the event loop regardless.
|
||
|
||
// 1. Let result be the result of opening a database connection, with storageKey, name, version if given and undefined otherwise, and request.
|
||
open_a_database_connection(realm, storage_key.value(), name, version, request, GC::create_function(realm.heap(), [&realm, request](WebIDL::ExceptionOr<GC::Ref<IDBDatabase>> result) {
|
||
// 2. Set request’s processed flag to true.
|
||
request->set_processed(true);
|
||
|
||
// 3. Queue a database task to run these steps:
|
||
queue_a_database_task(GC::create_function(realm.heap(), [&realm, request, result = move(result)]() mutable {
|
||
// 1. If result is an error, then:
|
||
if (result.is_error()) {
|
||
// 1. Set request’s result to undefined.
|
||
request->set_result(JS::js_undefined());
|
||
|
||
// 2. Set request’s error to result.
|
||
request->set_error(result.exception().get<GC::Ref<WebIDL::DOMException>>());
|
||
|
||
// 3. Set request’s done flag to true.
|
||
request->set_done(true);
|
||
|
||
// 4. Fire an event named error at request with its bubbles and cancelable attributes initialized to true.
|
||
request->dispatch_event(DOM::Event::create(realm, HTML::EventNames::error));
|
||
} else {
|
||
// 1. Set request’s result to result.
|
||
request->set_result(result.release_value());
|
||
|
||
// 2. Set request’s done flag to true.
|
||
request->set_done(true);
|
||
|
||
// 3. Fire an event named success at request.
|
||
request->dispatch_event(DOM::Event::create(realm, HTML::EventNames::success));
|
||
}
|
||
}));
|
||
}));
|
||
|
||
// 6. Return a new IDBOpenDBRequest object for request.
|
||
return request;
|
||
}
|
||
|
||
// https://w3c.github.io/IndexedDB/#dom-idbfactory-cmp
|
||
WebIDL::ExceptionOr<i8> IDBFactory::cmp(JS::Value first, JS::Value second)
|
||
{
|
||
// 1. Let a be the result of converting a value to a key with first. Rethrow any exceptions.
|
||
auto a = TRY(convert_a_value_to_a_key(realm(), first));
|
||
|
||
// 2. If a is invalid, throw a "DataError" DOMException.
|
||
if (a->is_invalid())
|
||
return WebIDL::DataError::create(realm(), "Failed to convert a value to a key"_utf16);
|
||
|
||
// 3. Let b be the result of converting a value to a key with second. Rethrow any exceptions.
|
||
auto b = TRY(convert_a_value_to_a_key(realm(), second));
|
||
|
||
// 4. If b is invalid, throw a "DataError" DOMException.
|
||
if (b->is_invalid())
|
||
return WebIDL::DataError::create(realm(), "Failed to convert a value to a key"_utf16);
|
||
|
||
// 5. Return the results of comparing two keys with a and b.
|
||
return Key::compare_two_keys(a, b);
|
||
}
|
||
|
||
// https://w3c.github.io/IndexedDB/#dom-idbfactory-deletedatabase
|
||
WebIDL::ExceptionOr<GC::Ref<IDBOpenDBRequest>> IDBFactory::delete_database(String const& name)
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. Let environment be this's relevant settings object.
|
||
auto& environment = HTML::relevant_settings_object(*this);
|
||
|
||
// 2. Let storageKey be the result of running obtain a storage key given environment.
|
||
// If failure is returned, then throw a "SecurityError" DOMException and abort these steps.
|
||
auto storage_key = StorageAPI::obtain_a_storage_key(environment);
|
||
if (!storage_key.has_value())
|
||
return WebIDL::SecurityError::create(realm, "Failed to obtain a storage key"_utf16);
|
||
|
||
// 3. Let request be a new open request.
|
||
auto request = IDBOpenDBRequest::create(realm);
|
||
|
||
// 4. Run these steps in parallel:
|
||
// NB: We could defer these steps, but it wouldn't be observable anyway, since delete_a_database() will
|
||
// put this work into a queue and then process it later in the event loop regardless.
|
||
|
||
// 1. Let result be the result of deleting a database, with storageKey, name, and request.
|
||
delete_a_database(realm, storage_key.value(), name, request, GC::create_function(realm.heap(), [&realm, request](WebIDL::ExceptionOr<u64> result) {
|
||
// 2. Set request’s processed flag to true.
|
||
request->set_processed(true);
|
||
|
||
// 3. Queue a database task to run these steps:
|
||
queue_a_database_task(GC::create_function(realm.heap(), [&realm, request, result = move(result)]() mutable {
|
||
// 1. If result is an error,
|
||
if (result.is_error()) {
|
||
// set request’s error to result,
|
||
request->set_error(result.exception().get<GC::Ref<WebIDL::DOMException>>());
|
||
// set request’s done flag to true,
|
||
request->set_done(true);
|
||
// and fire an event named error at request with its bubbles and cancelable attributes initialized to true.
|
||
request->dispatch_event(DOM::Event::create(realm, HTML::EventNames::error, { .bubbles = true, .cancelable = true }));
|
||
}
|
||
// 2. Otherwise,
|
||
else {
|
||
// set request’s result to undefined,
|
||
request->set_result(JS::js_undefined());
|
||
// set request’s done flag to true,
|
||
request->set_done(true);
|
||
// and fire a version change event named success at request with result and null.
|
||
auto value = result.release_value();
|
||
fire_a_version_change_event(realm, HTML::EventNames::success, request, value, {});
|
||
}
|
||
}));
|
||
}));
|
||
|
||
// 5. Return a new IDBOpenDBRequest object for request.
|
||
return request;
|
||
}
|
||
|
||
// https://w3c.github.io/IndexedDB/#dom-idbfactory-databases
|
||
GC::Ref<WebIDL::Promise> IDBFactory::databases()
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. Let environment be this's relevant settings object.
|
||
auto& environment = HTML::relevant_settings_object(*this);
|
||
|
||
// 2. Let storageKey be the result of running obtain a storage key given environment.
|
||
// If failure is returned, then return a promise rejected with a "SecurityError" DOMException
|
||
auto maybe_storage_key = StorageAPI::obtain_a_storage_key(environment);
|
||
if (!maybe_storage_key.has_value())
|
||
return WebIDL::create_rejected_promise_from_exception(realm, WebIDL::SecurityError::create(realm, "Failed to obtain a storage key"_utf16));
|
||
|
||
auto storage_key = maybe_storage_key.release_value();
|
||
|
||
// 3. Let p be a new promise.
|
||
auto p = WebIDL::create_promise(realm);
|
||
|
||
// 4. Run these steps in parallel:
|
||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, storage_key, p]() {
|
||
HTML::TemporaryExecutionContext context(realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes);
|
||
|
||
// 1. Let databases be the set of databases in storageKey.
|
||
// If this cannot be determined for any reason, then reject p with an appropriate error (e.g. an "UnknownError" DOMException) and terminate these steps.
|
||
auto databases = Database::for_key(storage_key);
|
||
|
||
// 2. Let result be a new list.
|
||
auto result = MUST(JS::Array::create(realm, databases.size()));
|
||
|
||
// 3. For each db of databases:
|
||
for (u32 i = 0; i < databases.size(); ++i) {
|
||
auto& db = databases[i];
|
||
|
||
// 1. If db’s version is 0, then continue.
|
||
if (db->version() == 0)
|
||
continue;
|
||
|
||
// 2. Let info be a new IDBDatabaseInfo dictionary.
|
||
auto info = JS::Object::create(realm, realm.intrinsics().object_prototype());
|
||
|
||
// 3. Set info’s name dictionary member to db’s name.
|
||
MUST(info->create_data_property("name"_utf16_fly_string, JS::PrimitiveString::create(realm.vm(), db->name())));
|
||
|
||
// 4. Set info’s version dictionary member to db’s version.
|
||
MUST(info->create_data_property("version"_utf16_fly_string, JS::Value(db->version())));
|
||
|
||
// 4. Append info to result.
|
||
MUST(result->create_data_property_or_throw(i, info));
|
||
}
|
||
|
||
// 4. Resolve p with result.
|
||
WebIDL::resolve_promise(realm, p, result);
|
||
}));
|
||
|
||
// 5. Return p.
|
||
return p;
|
||
}
|
||
|
||
}
|