The process-wide IndexedDB database registry stores internal database objects in GC roots. Those objects do not need a JavaScript shape or realm, but they inherited one from PlatformObject. This let an internal database keep its creating realm, window, and script graph alive. Make the internal database construct a plain GC cell instead. Callers provide a heap when they need snapshots of associated connections. This means the database no longer needs to remember its creating realm. On x.com, the rooted internal database subgraph shrank from the whole page graph, including 24k executables, to the database metadata itself.
210 lines
6.4 KiB
C++
210 lines
6.4 KiB
C++
/*
|
|
* Copyright (c) 2024-2025, stelar7 <dudedbz@gmail.com>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <LibGC/Heap.h>
|
|
#include <LibWeb/IndexedDB/IDBDatabase.h>
|
|
#include <LibWeb/IndexedDB/IDBTransaction.h>
|
|
#include <LibWeb/IndexedDB/Internal/Algorithms.h>
|
|
#include <LibWeb/IndexedDB/Internal/ConnectionQueueHandler.h>
|
|
#include <LibWeb/IndexedDB/Internal/Database.h>
|
|
|
|
namespace Web::IndexedDB {
|
|
|
|
using IDBDatabaseMapping = HashMap<StorageAPI::StorageKey, HashMap<String, GC::Root<Database>>>;
|
|
static IDBDatabaseMapping m_databases;
|
|
|
|
void Database::for_each_database(AK::Function<void(Database&)> const& visitor)
|
|
{
|
|
for (auto const& [key, mapping] : m_databases) {
|
|
for (auto const& [_, database] : mapping) {
|
|
if (!database)
|
|
continue;
|
|
visitor(*database);
|
|
}
|
|
}
|
|
}
|
|
|
|
GC_DEFINE_ALLOCATOR(Database);
|
|
|
|
Database::~Database() = default;
|
|
|
|
GC::Ref<Database> Database::create(GC::Heap& heap, String const& name)
|
|
{
|
|
return heap.allocate<Database>(name);
|
|
}
|
|
|
|
void Database::visit_edges(Visitor& visitor)
|
|
{
|
|
Base::visit_edges(visitor);
|
|
visitor.visit(m_upgrade_transaction);
|
|
visitor.visit(m_object_stores);
|
|
|
|
if (m_pending_connection_wait.has_value())
|
|
visitor.visit(m_pending_connection_wait->callback);
|
|
}
|
|
|
|
GC::Ptr<ObjectStore> Database::object_store_with_name(String const& name) const
|
|
{
|
|
for (auto const& object_store : m_object_stores) {
|
|
if (object_store->name() == name)
|
|
return object_store;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
Vector<GC::Weak<Database>> Database::for_key(StorageAPI::StorageKey const& key)
|
|
{
|
|
Vector<GC::Weak<Database>> databases;
|
|
for (auto const& database_mapping : m_databases.get(key).value_or({})) {
|
|
databases.append(*database_mapping.value);
|
|
}
|
|
|
|
return databases;
|
|
}
|
|
|
|
RequestList& ConnectionQueueHandler::for_key_and_name(StorageAPI::StorageKey const& key, String const& name)
|
|
{
|
|
auto& instance = ConnectionQueueHandler::the();
|
|
auto maybe_connection = instance.m_open_requests.find_if([&key, &name](Connection const& connection) {
|
|
return connection.storage_key == key && connection.name == name;
|
|
});
|
|
|
|
if (!maybe_connection.is_end())
|
|
return (*maybe_connection)->request_list;
|
|
|
|
auto new_connection = adopt_ref(*new Connection(key, name));
|
|
instance.m_open_requests.append(new_connection);
|
|
return new_connection->request_list;
|
|
}
|
|
|
|
Optional<Database&> Database::for_key_and_name(StorageAPI::StorageKey const& key, String const& name)
|
|
{
|
|
auto database_mapping = m_databases.ensure(key, [] { return HashMap<String, GC::Root<Database>>(); });
|
|
if (auto maybe_database = database_mapping.get(name); maybe_database.has_value())
|
|
return *maybe_database.value();
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<GC::Ref<Database>> Database::create_for_key_and_name(GC::Heap& heap, StorageAPI::StorageKey const& key, String const& name)
|
|
{
|
|
auto database_mapping = TRY(m_databases.try_ensure(key, [] {
|
|
return HashMap<String, GC::Root<Database>>();
|
|
}));
|
|
|
|
auto value = Database::create(heap, name);
|
|
|
|
database_mapping.set(name, value);
|
|
m_databases.set(key, database_mapping);
|
|
|
|
return value;
|
|
}
|
|
|
|
ErrorOr<void> Database::delete_for_key_and_name(StorageAPI::StorageKey const& key, String const& name)
|
|
{
|
|
// FIXME: Is a missing entry a failure?
|
|
auto maybe_database_mapping = m_databases.get(key);
|
|
if (!maybe_database_mapping.has_value())
|
|
return {};
|
|
|
|
auto& database_mapping = maybe_database_mapping.value();
|
|
auto maybe_database = database_mapping.get(name);
|
|
if (!maybe_database.has_value())
|
|
return {};
|
|
|
|
auto did_remove = database_mapping.remove(name);
|
|
if (!did_remove)
|
|
return {};
|
|
|
|
m_databases.set(key, database_mapping);
|
|
|
|
return {};
|
|
}
|
|
|
|
void Database::associate(GC::Ref<IDBDatabase> connection)
|
|
{
|
|
m_associated_connections.append(connection);
|
|
}
|
|
|
|
void Database::dissociate(IDBDatabase& connection)
|
|
{
|
|
m_associated_connections.remove_first_matching([&](auto& entry) { return entry == &connection; });
|
|
}
|
|
|
|
GC::Ref<Database::AssociatedConnections> Database::associated_connections_as_heap_vector(GC::Heap& heap)
|
|
{
|
|
auto connections = heap.allocate<AssociatedConnections>();
|
|
for (auto& associated_connection : m_associated_connections) {
|
|
if (associated_connection)
|
|
connections->elements().append(*associated_connection);
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
GC::RootVector<GC::Ref<IDBDatabase>> Database::associated_connections_as_root_vector(GC::Heap& heap)
|
|
{
|
|
GC::RootVector<GC::Ref<IDBDatabase>> connections(heap);
|
|
for (auto& connection : m_associated_connections) {
|
|
if (connection)
|
|
connections.append(*connection);
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
GC::Ref<Database::AssociatedConnections> Database::associated_connections_as_heap_vector_except(GC::Heap& heap, IDBDatabase& connection)
|
|
{
|
|
auto connections = heap.allocate<AssociatedConnections>();
|
|
for (auto& associated_connection : m_associated_connections) {
|
|
if (associated_connection && associated_connection != &connection)
|
|
connections->elements().append(*associated_connection);
|
|
}
|
|
return connections;
|
|
}
|
|
|
|
void Database::wait_for_connections_to_close(ReadonlySpan<GC::Ref<IDBDatabase>> connections, GC::Ref<GC::Function<void()>> after_all)
|
|
{
|
|
bool all_closed = true;
|
|
for (auto const& entry : connections) {
|
|
if (entry->state() != ConnectionState::Closed) {
|
|
all_closed = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (all_closed) {
|
|
queue_a_database_task(after_all);
|
|
return;
|
|
}
|
|
|
|
VERIFY(!m_pending_connection_wait.has_value());
|
|
Vector<GC::Weak<IDBDatabase>> weak_connections;
|
|
weak_connections.ensure_capacity(connections.size());
|
|
for (auto const& connection : connections)
|
|
weak_connections.unchecked_append(connection);
|
|
m_pending_connection_wait = PendingConnectionWait {
|
|
.connections = move(weak_connections),
|
|
.callback = after_all,
|
|
};
|
|
}
|
|
|
|
void Database::check_pending_connection_wait()
|
|
{
|
|
if (!m_pending_connection_wait.has_value())
|
|
return;
|
|
|
|
auto& wait = m_pending_connection_wait.value();
|
|
for (auto const& connection : wait.connections) {
|
|
if (connection && connection->state() != ConnectionState::Closed)
|
|
return;
|
|
}
|
|
|
|
// All connections are closed - invoke the callback and clear the wait
|
|
auto callback = wait.callback;
|
|
m_pending_connection_wait.clear();
|
|
queue_a_database_task(callback);
|
|
}
|
|
|
|
}
|