From e667aaaab1761a08740c96349bc4f230e9fe5e09 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Thu, 11 Jun 2026 16:51:05 +0100 Subject: [PATCH] LibDevTools+LibWeb: Send IndexedDB change updates The Storage panel expects storesUpdate messages after watched storage changes. Summarize committed IndexedDB mutation logs into DevTools paths and forward them through WebContent so Firefox can refresh database, object-store, and record rows without polling. Firefox's behaviour is less than ideal here. A lot of things don't update automatically even inspecting a page in Firefox. Some things (like new databases) won't show up until you fully refresh the page. So that makes it a bit hard to know that we're doing things correctly. As far as I can tell, we are at least behaving as well as Firefox requires. We do have one workaround: Firefox doesn't display record updates without a manual refresh, and in fact any change messages for them show up as rows in the host's database table. So for now, we filter them out to avoid visual weirdness in the inspector. --- .../LibDevTools/Actors/IndexedDBActor.cpp | 24 +++- Libraries/LibDevTools/Actors/IndexedDBActor.h | 2 + Libraries/LibDevTools/DevToolsDelegate.h | 3 + .../LibDevTools/IndexedDBSerialization.cpp | 42 ++++++ .../LibDevTools/IndexedDBSerialization.h | 2 + Libraries/LibWeb/IndexedDB/IDBTransaction.cpp | 22 +++ Libraries/LibWeb/IndexedDB/IDBTransaction.h | 2 + .../LibWeb/IndexedDB/Internal/Algorithms.cpp | 1 + .../LibWeb/IndexedDB/Internal/MutationLog.cpp | 67 +++++++++ .../LibWeb/IndexedDB/Internal/MutationLog.h | 3 + .../LibWeb/IndexedDB/TransactionChanges.h | 33 +++++ Libraries/LibWeb/Page/Page.h | 2 + Libraries/LibWebView/Application.cpp | 46 ++++++ Libraries/LibWebView/Application.h | 5 + Libraries/LibWebView/ViewImplementation.cpp | 18 +++ Libraries/LibWebView/ViewImplementation.h | 5 + Libraries/LibWebView/WebContentClient.cpp | 6 + Libraries/LibWebView/WebContentClient.h | 1 + Services/WebContent/PageClient.cpp | 13 ++ Services/WebContent/PageClient.h | 1 + Services/WebContent/WebContentClient.ipc | 1 + Tests/LibDevTools/TestDevToolsProtocol.cpp | 132 ++++++++++++++++++ 22 files changed, 430 insertions(+), 1 deletion(-) create mode 100644 Libraries/LibWeb/IndexedDB/TransactionChanges.h diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.cpp b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp index 3bcb3f81ea..ed7efd7459 100644 --- a/Libraries/LibDevTools/Actors/IndexedDBActor.cpp +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.cpp @@ -23,9 +23,23 @@ IndexedDBActor::IndexedDBActor(DevToolsServer& devtools, String name, WeakPtrdescription(), + weak_callback(*this, [](auto& self, JsonObject update) { + self.on_indexed_database_changed(move(update)); + })); + } } -IndexedDBActor::~IndexedDBActor() = default; +IndexedDBActor::~IndexedDBActor() +{ + if (m_indexed_database_change_listener_id == 0) + return; + + if (auto tab = m_tab.strong_ref()) + devtools().delegate().remove_indexed_database_change_listener(tab->description(), m_indexed_database_change_listener_id); +} JsonObject IndexedDBActor::serialize_storage(JsonObject hosts) const { @@ -155,4 +169,12 @@ void IndexedDBActor::get_store_objects(Message const& message) }); } +void IndexedDBActor::on_indexed_database_changed(JsonObject update) +{ + JsonObject message; + message.set("type"sv, "storesUpdate"sv); + message.set("data"sv, move(update)); + send_message(move(message)); +} + } diff --git a/Libraries/LibDevTools/Actors/IndexedDBActor.h b/Libraries/LibDevTools/Actors/IndexedDBActor.h index 417714feda..725103d841 100644 --- a/Libraries/LibDevTools/Actors/IndexedDBActor.h +++ b/Libraries/LibDevTools/Actors/IndexedDBActor.h @@ -27,11 +27,13 @@ private: void get_fields(Message const&); void get_store_objects(Message const&); + void on_indexed_database_changed(JsonObject); JsonObject serialize_storage(JsonObject hosts) const; void send_inspection_error(Message const&, Error const&); WeakPtr m_tab; + u64 m_indexed_database_change_listener_id { 0 }; }; } diff --git a/Libraries/LibDevTools/DevToolsDelegate.h b/Libraries/LibDevTools/DevToolsDelegate.h index db93abe17d..da0f6adb39 100644 --- a/Libraries/LibDevTools/DevToolsDelegate.h +++ b/Libraries/LibDevTools/DevToolsDelegate.h @@ -78,6 +78,9 @@ public: using OnIndexedDBInspectionComplete = Function)>; virtual void inspect_indexed_database_storage(TabDescription const&, OnIndexedDBInspectionComplete) const { } virtual void inspect_indexed_database_objects(TabDescription const&, String const&, Optional, JsonObject, OnIndexedDBInspectionComplete) const { } + using OnIndexedDatabaseChange = Function; + virtual u64 add_indexed_database_change_listener(TabDescription const&, OnIndexedDatabaseChange) const { return 0; } + virtual void remove_indexed_database_change_listener(TabDescription const&, u64) const { } using OnTabInspectionComplete = Function)>; virtual void inspect_tab(TabDescription const&, OnTabInspectionComplete) const { } diff --git a/Libraries/LibDevTools/IndexedDBSerialization.cpp b/Libraries/LibDevTools/IndexedDBSerialization.cpp index 6648f48e90..457e5f8583 100644 --- a/Libraries/LibDevTools/IndexedDBSerialization.cpp +++ b/Libraries/LibDevTools/IndexedDBSerialization.cpp @@ -187,6 +187,48 @@ JsonObject serialize_objects(Web::DOM::Document& document, String const& host, J host); } +static void append_indexed_database_update(JsonObject& update, StringView type, JsonArray paths, String const& host) +{ + if (paths.is_empty()) + return; + + JsonObject hosts; + hosts.set(host, move(paths)); + + JsonObject indexed_database; + indexed_database.set("indexedDB"sv, move(hosts)); + + update.set(type, move(indexed_database)); +} + +static JsonArray serialize_update_paths(Vector const& changes) +{ + JsonArray paths; + paths.ensure_capacity(changes.size()); + for (auto const& change : changes) { + // FIXME: Firefox treats added IndexedDB update paths as storage tree additions, so record-level changes must + // not be sent there or they appear as blank rows in the selected host view. + // Remove this filter once Firefox supports record updates. + if (change.key.has_value()) + continue; + paths.must_append(indexed_database_path(change.database_name, change.object_store_name, change.key)); + } + return paths; +} + +JsonObject serialize_update(String const& url, Web::IndexedDB::TransactionChanges const& changes) +{ + JsonObject update; + auto host = storage_host_for_url(url); + if (!host.has_value()) + return update; + + append_indexed_database_update(update, "added"sv, serialize_update_paths(changes.added), *host); + append_indexed_database_update(update, "changed"sv, serialize_update_paths(changes.changed), *host); + append_indexed_database_update(update, "deleted"sv, serialize_update_paths(changes.deleted), *host); + return update; +} + static ErrorOr parse_required_indexed_database_path(String const& name) { auto path = parse_indexed_database_path(JsonValue { name }); diff --git a/Libraries/LibDevTools/IndexedDBSerialization.h b/Libraries/LibDevTools/IndexedDBSerialization.h index a87d57ccbd..2649e1582d 100644 --- a/Libraries/LibDevTools/IndexedDBSerialization.h +++ b/Libraries/LibDevTools/IndexedDBSerialization.h @@ -11,11 +11,13 @@ #include #include #include +#include namespace DevTools::IndexedDB { DEVTOOLS_API JsonObject serialize_storage(Web::DOM::Document&); DEVTOOLS_API JsonObject serialize_objects(Web::DOM::Document&, String const& host, JsonValue const& names, JsonValue const& options); +DEVTOOLS_API JsonObject serialize_update(String const& url, Web::IndexedDB::TransactionChanges const&); DEVTOOLS_API ErrorOr delete_database(Web::DOM::Document&, String const& host, String const& name); DEVTOOLS_API ErrorOr clear_object_store(Web::DOM::Document&, String const& host, String const& name); DEVTOOLS_API ErrorOr delete_record(Web::DOM::Document&, String const& host, String const& name); diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp index ecd19e1432..76531b6ece 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.cpp @@ -7,12 +7,15 @@ #include #include #include +#include #include +#include #include #include #include #include #include +#include namespace Web::IndexedDB { @@ -238,4 +241,23 @@ void IDBTransaction::discard_mutation_logs() m_store_mutation_logs.clear(); } +void IDBTransaction::notify_devtools_of_committed_changes() +{ + auto document = HTML::relevant_settings_object(*this).responsible_document(); + if (!document) + return; + + if (!document->page().client().has_active_devtools_client()) + return; + + TransactionChanges changes; + auto database_name = m_connection->associated_database()->name(); + for (auto const& entry : m_store_mutation_logs) + entry.log->append_changes(database_name, entry.store->name(), changes); + if (changes.is_empty()) + return; + + document->page().client().page_did_update_indexed_database(document->url().serialize(), changes); +} + } diff --git a/Libraries/LibWeb/IndexedDB/IDBTransaction.h b/Libraries/LibWeb/IndexedDB/IDBTransaction.h index a3fb53b91c..8f8692e9ed 100644 --- a/Libraries/LibWeb/IndexedDB/IDBTransaction.h +++ b/Libraries/LibWeb/IndexedDB/IDBTransaction.h @@ -108,6 +108,8 @@ public: void set_onerror(WebIDL::CallbackType*); WebIDL::CallbackType* onerror(); + void notify_devtools_of_committed_changes(); + protected: explicit IDBTransaction(JS::Realm&, GC::Ref, Bindings::IDBTransactionMode, Bindings::IDBTransactionDurability, Vector>); virtual void initialize(JS::Realm&) override; diff --git a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp index 43a55f88e0..86bf0b7cf5 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/Algorithms.cpp @@ -885,6 +885,7 @@ void commit_a_transaction(JS::Realm& realm, GC::Ref transaction) transaction->connection()->associated_database()->set_upgrade_transaction(nullptr); // AD-HOC: Discard mutation logs now that changes are permanent. + transaction->notify_devtools_of_committed_changes(); transaction->discard_mutation_logs(); // 2. Set transaction’s state to finished. diff --git a/Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp b/Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp index 04f84af1b7..355dc0ac15 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp +++ b/Libraries/LibWeb/IndexedDB/Internal/MutationLog.cpp @@ -5,8 +5,10 @@ */ #include +#include #include #include +#include #include #include @@ -120,6 +122,71 @@ void MutationLog::note_index_record_stored(GC::Ref index, IndexRecord rec m_entries.append(IndexRecordStored { index, record }); } +void MutationLog::append_changes(String const& database_name, String const& object_store_name, TransactionChanges& changes) const +{ + Vector> stored_keys; + Vector> changed_keys; + Vector> deleted_keys; + + auto remove_key = [](Vector>& keys, Key& key) { + return keys.remove_first_matching([&](auto const& existing) { + return Key::equals(existing, GC::Ref { key }); + }); + }; + + auto append_deleted_key = [&](Key& key) { + if (remove_key(stored_keys, key)) + return; + remove_key(changed_keys, key); + deleted_keys.append(GC::Ref { key }); + }; + + auto append_stored_key = [&](Key& key) { + if (remove_key(deleted_keys, key)) { + changed_keys.append(GC::Ref { key }); + return; + } + stored_keys.append(GC::Ref { key }); + }; + + for (auto const& entry : m_entries) { + entry.visit( + [&](ObjectStoreCreated const&) { + changes.added.append({ database_name, object_store_name }); + }, + [&](ObjectStoreDeleted const&) { + changes.deleted.append({ database_name, object_store_name }); + }, + [&](ObjectStoreRenamed const& e) { + changes.deleted.append({ database_name, e.old_name }); + changes.added.append({ database_name, object_store_name }); + }, + [&](IndexCreated const&) {}, + [&](IndexDeleted const&) {}, + [&](IndexRenamed const&) {}, + [&](KeyGeneratorChanged const&) { + }, + [&](RecordsDeleted const& e) { + for (auto const& record : e.records) + append_deleted_key(*record.key); + }, + [&](RecordStored const& e) { + append_stored_key(*e.key); + }, + [&](IndexRecordsDeleted const&) {}, + [&](IndexRecordStored const&) {}); + } + + for (auto const& key : stored_keys) + changes.added.append({ database_name, object_store_name, serialize_key_for_inspection(key) }); + + for (auto const& key : changed_keys) + changes.changed.append({ database_name, object_store_name, serialize_key_for_inspection(key) }); + + for (auto const& key : deleted_keys) + changes.deleted.append({ database_name, object_store_name, serialize_key_for_inspection(key) }); +} + void MutationLog::revert(ObjectStore& store, GC::Ref database, GC::Ref connection) { revert_entries(store, 0, database, connection); diff --git a/Libraries/LibWeb/IndexedDB/Internal/MutationLog.h b/Libraries/LibWeb/IndexedDB/Internal/MutationLog.h index bcc02771db..0f818d62d8 100644 --- a/Libraries/LibWeb/IndexedDB/Internal/MutationLog.h +++ b/Libraries/LibWeb/IndexedDB/Internal/MutationLog.h @@ -12,6 +12,7 @@ #include #include #include +#include namespace Web::IndexedDB { @@ -67,6 +68,8 @@ public: // Clear the log without reverting (used after successful transaction commit). void clear() { m_entries.clear(); } + void append_changes(String const& database_name, String const& object_store_name, TransactionChanges&) const; + [[nodiscard]] size_t position() const { return m_entries.size(); } protected: diff --git a/Libraries/LibWeb/IndexedDB/TransactionChanges.h b/Libraries/LibWeb/IndexedDB/TransactionChanges.h new file mode 100644 index 0000000000..fd7466e0f4 --- /dev/null +++ b/Libraries/LibWeb/IndexedDB/TransactionChanges.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026, Ladybird contributors + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +namespace Web::IndexedDB { + +struct TransactionChange { + String database_name; + String object_store_name; + Optional key {}; +}; + +struct TransactionChanges { + Vector added; + Vector changed; + Vector deleted; + + bool is_empty() const + { + return added.is_empty() && changed.is_empty() && deleted.is_empty(); + } +}; + +} diff --git a/Libraries/LibWeb/Page/Page.h b/Libraries/LibWeb/Page/Page.h index 44de34c43b..a11ca94180 100644 --- a/Libraries/LibWeb/Page/Page.h +++ b/Libraries/LibWeb/Page/Page.h @@ -49,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -504,6 +505,7 @@ public: virtual Vector page_did_request_storage_keys([[maybe_unused]] Web::StorageAPI::StorageEndpointType storage_endpoint, [[maybe_unused]] String const& storage_key) { return {}; } virtual void page_did_clear_storage([[maybe_unused]] Web::StorageAPI::StorageEndpointType storage_endpoint, [[maybe_unused]] String const& storage_key) { } virtual void page_did_broadcast_storage_change([[maybe_unused]] Web::StorageAPI::StorageEndpointType storage_endpoint, [[maybe_unused]] String const& url, [[maybe_unused]] Optional const& key, [[maybe_unused]] Optional const& old_value, [[maybe_unused]] Optional const& new_value) { } + virtual void page_did_update_indexed_database([[maybe_unused]] String const& url, [[maybe_unused]] IndexedDB::TransactionChanges const&) { } virtual void page_did_update_resource_count(i32) { } struct NewWebViewResult { GC::Ptr page; diff --git a/Libraries/LibWebView/Application.cpp b/Libraries/LibWebView/Application.cpp index 3e56aeea60..9fab7d0f6e 100644 --- a/Libraries/LibWebView/Application.cpp +++ b/Libraries/LibWebView/Application.cpp @@ -1998,6 +1998,52 @@ void Application::inspect_indexed_database_objects(DevTools::TabDescription cons view->inspect_indexed_database_objects(host, move(names), move(options), move(on_complete)); } +void Application::delete_indexed_database(DevTools::TabDescription const& description, String const& host, String const& name, OnIndexedDBInspectionComplete on_complete) const +{ + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + view->delete_indexed_database(host, name, move(on_complete)); +} + +void Application::clear_indexed_database_object_store(DevTools::TabDescription const& description, String const& host, String const& name, OnIndexedDBInspectionComplete on_complete) const +{ + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + view->clear_indexed_database_object_store(host, name, move(on_complete)); +} + +void Application::delete_indexed_database_record(DevTools::TabDescription const& description, String const& host, String const& name, OnIndexedDBInspectionComplete on_complete) const +{ + auto view = ViewImplementation::find_view_by_id(description.id); + if (!view.has_value()) { + on_complete(Error::from_string_literal("Unable to locate tab")); + return; + } + + view->delete_indexed_database_record(host, name, move(on_complete)); +} + +u64 Application::add_indexed_database_change_listener(DevTools::TabDescription const& description, OnIndexedDatabaseChange on_indexed_database_change) const +{ + if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value()) + return view->add_indexed_database_change_listener(move(on_indexed_database_change)); + return 0; +} + +void Application::remove_indexed_database_change_listener(DevTools::TabDescription const& description, u64 listener_id) const +{ + if (auto view = ViewImplementation::find_view_by_id(description.id); view.has_value()) + view->remove_indexed_database_change_listener(listener_id); +} + void Application::inspect_tab(DevTools::TabDescription const& description, OnTabInspectionComplete on_complete) const { auto view = ViewImplementation::find_view_by_id(description.id); diff --git a/Libraries/LibWebView/Application.h b/Libraries/LibWebView/Application.h index e695cd61ce..4c459943e8 100644 --- a/Libraries/LibWebView/Application.h +++ b/Libraries/LibWebView/Application.h @@ -285,6 +285,11 @@ private: virtual void remove_storage_change_listener(DevTools::TabDescription const&, u64) const override; virtual void inspect_indexed_database_storage(DevTools::TabDescription const&, OnIndexedDBInspectionComplete) const override; virtual void inspect_indexed_database_objects(DevTools::TabDescription const&, String const&, Optional, JsonObject, OnIndexedDBInspectionComplete) const override; + virtual void delete_indexed_database(DevTools::TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const override; + virtual void clear_indexed_database_object_store(DevTools::TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const override; + virtual void delete_indexed_database_record(DevTools::TabDescription const&, String const&, String const&, OnIndexedDBInspectionComplete) const override; + virtual u64 add_indexed_database_change_listener(DevTools::TabDescription const&, OnIndexedDatabaseChange) const override; + virtual void remove_indexed_database_change_listener(DevTools::TabDescription const&, u64) const override; virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete) const override; virtual void inspect_accessibility_tree(DevTools::TabDescription const&, OnAccessibilityTreeInspectionComplete) const override; virtual void listen_for_dom_properties(DevTools::TabDescription const&, OnDOMNodePropertiesReceived) const override; diff --git a/Libraries/LibWebView/ViewImplementation.cpp b/Libraries/LibWebView/ViewImplementation.cpp index 26b5ddce74..faa089da8c 100644 --- a/Libraries/LibWebView/ViewImplementation.cpp +++ b/Libraries/LibWebView/ViewImplementation.cpp @@ -718,6 +718,24 @@ void ViewImplementation::remove_storage_change_listener(u64 listener_id) m_storage_change_listeners.remove(listener_id); } +void ViewImplementation::notify_indexed_database_changed(JsonObject update) +{ + for (auto& listener : m_indexed_database_change_listeners) + listener.value(update); +} + +u64 ViewImplementation::add_indexed_database_change_listener(DevTools::DevToolsDelegate::OnIndexedDatabaseChange on_indexed_database_change) +{ + auto listener_id = m_next_indexed_database_change_listener_id++; + m_indexed_database_change_listeners.set(listener_id, move(on_indexed_database_change)); + return listener_id; +} + +void ViewImplementation::remove_indexed_database_change_listener(u64 listener_id) +{ + m_indexed_database_change_listeners.remove(listener_id); +} + ErrorOr ViewImplementation::ensure_document_cookie_version_index(Badge, String const& domain) { return m_document_cookie_version_indices.try_ensure(domain, [&]() -> ErrorOr { diff --git a/Libraries/LibWebView/ViewImplementation.h b/Libraries/LibWebView/ViewImplementation.h index 6cc5f568b5..8fe866a6a5 100644 --- a/Libraries/LibWebView/ViewImplementation.h +++ b/Libraries/LibWebView/ViewImplementation.h @@ -140,6 +140,9 @@ public: void notify_cookies_changed(HashTable const& changed_domains, ReadonlySpan page_cookies, ReadonlySpan host_cookies); void listen_for_host_cookie_changes(DevTools::DevToolsDelegate::OnHostCookieChange); void stop_listening_for_host_cookie_changes(); + void notify_indexed_database_changed(JsonObject); + u64 add_indexed_database_change_listener(DevTools::DevToolsDelegate::OnIndexedDatabaseChange); + void remove_indexed_database_change_listener(u64 listener_id); ErrorOr ensure_document_cookie_version_index(Badge, String const&); Optional document_cookie_version(URL::URL const&) const; @@ -604,6 +607,8 @@ protected: Core::AnonymousBuffer m_document_cookie_version_buffer; HashMap m_document_cookie_version_indices; DevTools::DevToolsDelegate::OnHostCookieChange m_on_host_cookie_change; + HashMap m_indexed_database_change_listeners; + u64 m_next_indexed_database_change_listener_id { 1 }; HashMap m_storage_change_listeners; u64 m_next_storage_change_listener_id { 1 }; diff --git a/Libraries/LibWebView/WebContentClient.cpp b/Libraries/LibWebView/WebContentClient.cpp index 5bee424747..c0343461c4 100644 --- a/Libraries/LibWebView/WebContentClient.cpp +++ b/Libraries/LibWebView/WebContentClient.cpp @@ -1086,6 +1086,12 @@ void WebContentClient::did_change_storage_item(u64 page_id, Web::StorageAPI::Sto } } +void WebContentClient::did_update_indexed_database(u64 page_id, String update) +{ + if (auto view = view_for_page_id(page_id); view.has_value()) + view->notify_indexed_database_changed(parse_json(update, "IndexedDB update"sv)); +} + void WebContentClient::did_post_broadcast_channel_message(u64, Web::HTML::BroadcastChannelMessage message) { WebContentClient::for_each_client([&](auto& client) { diff --git a/Libraries/LibWebView/WebContentClient.h b/Libraries/LibWebView/WebContentClient.h index 5ded9edd0e..3513d42c55 100644 --- a/Libraries/LibWebView/WebContentClient.h +++ b/Libraries/LibWebView/WebContentClient.h @@ -168,6 +168,7 @@ private: virtual Messages::WebContentClient::DidRequestStorageKeysResponse did_request_storage_keys(Web::StorageAPI::StorageEndpointType storage_endpoint, String storage_key) override; virtual void did_clear_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, String storage_key) override; virtual void did_change_storage_item(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, String url, Optional key, Optional old_value, Optional new_value) override; + virtual void did_update_indexed_database(u64 page_id, String update) override; virtual void did_post_broadcast_channel_message(u64 page_id, Web::HTML::BroadcastChannelMessage message) override; virtual Messages::WebContentClient::DidRequestNewWebViewResponse did_request_new_web_view(u64 page_id, Web::HTML::ActivateTab, Web::HTML::WebViewHints) override; virtual void did_request_activate_tab(u64 page_id) override; diff --git a/Services/WebContent/PageClient.cpp b/Services/WebContent/PageClient.cpp index d425904bdf..dc9bf53863 100644 --- a/Services/WebContent/PageClient.cpp +++ b/Services/WebContent/PageClient.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -778,6 +779,18 @@ void PageClient::page_did_broadcast_storage_change(Web::StorageAPI::StorageEndpo client().async_did_change_storage_item(m_id, storage_endpoint, url, key, old_value, new_value); } +void PageClient::page_did_update_indexed_database(String const& url, Web::IndexedDB::TransactionChanges const& changes) +{ + if (!has_devtools_client()) + return; + + auto update = DevTools::IndexedDB::serialize_update(url, changes); + if (update.is_empty()) + return; + + client().async_did_update_indexed_database(m_id, update.serialized()); +} + void PageClient::page_did_post_broadcast_channel_message(Web::HTML::BroadcastChannelMessage const& message) { client().async_did_post_broadcast_channel_message(m_id, message); diff --git a/Services/WebContent/PageClient.h b/Services/WebContent/PageClient.h index 51d04ff4ad..96d90788de 100644 --- a/Services/WebContent/PageClient.h +++ b/Services/WebContent/PageClient.h @@ -204,6 +204,7 @@ private: virtual Vector page_did_request_storage_keys(Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key) override; virtual void page_did_clear_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, String const& storage_key) override; virtual void page_did_broadcast_storage_change(Web::StorageAPI::StorageEndpointType storage_endpoint, String const& url, Optional const& key, Optional const& old_value, Optional const& new_value) override; + virtual void page_did_update_indexed_database(String const& url, Web::IndexedDB::TransactionChanges const&) override; virtual void page_did_update_resource_count(i32) override; virtual NewWebViewResult page_did_request_new_web_view(Web::HTML::ActivateTab, Web::HTML::WebViewHints, Web::HTML::TokenizedFeature::NoOpener) override; virtual void page_did_request_activate_tab() override; diff --git a/Services/WebContent/WebContentClient.ipc b/Services/WebContent/WebContentClient.ipc index 78085e3d44..dfd9f69bf3 100644 --- a/Services/WebContent/WebContentClient.ipc +++ b/Services/WebContent/WebContentClient.ipc @@ -114,6 +114,7 @@ endpoint WebContentClient did_request_storage_keys(Web::StorageAPI::StorageEndpointType storage_endpoint, String storage_key) => (Vector keys) did_clear_storage(Web::StorageAPI::StorageEndpointType storage_endpoint, String storage_key) => () did_change_storage_item(u64 page_id, Web::StorageAPI::StorageEndpointType storage_endpoint, String url, Optional key, Optional old_value, Optional new_value) =| + did_update_indexed_database(u64 page_id, String update) =| did_post_broadcast_channel_message(u64 page_id, Web::HTML::BroadcastChannelMessage message) =| did_update_resource_count(u64 page_id, i32 count_waiting) =| diff --git a/Tests/LibDevTools/TestDevToolsProtocol.cpp b/Tests/LibDevTools/TestDevToolsProtocol.cpp index 95e52e87c4..458d82c62a 100644 --- a/Tests/LibDevTools/TestDevToolsProtocol.cpp +++ b/Tests/LibDevTools/TestDevToolsProtocol.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -831,6 +832,44 @@ public: callback(make_indexed_database_store_objects(names)); } + virtual void delete_indexed_database(DevTools::TabDescription const&, String const& host, String const& name, OnIndexedDBInspectionComplete callback) const override + { + ++delete_indexed_database_call_count; + last_indexed_database_host = host; + last_indexed_database_name = name; + callback(JsonObject {}); + } + + virtual void clear_indexed_database_object_store(DevTools::TabDescription const&, String const& host, String const& name, OnIndexedDBInspectionComplete callback) const override + { + ++clear_indexed_database_object_store_call_count; + last_indexed_database_host = host; + last_indexed_database_name = name; + callback(JsonObject {}); + } + + virtual void delete_indexed_database_record(DevTools::TabDescription const&, String const& host, String const& name, OnIndexedDBInspectionComplete callback) const override + { + ++delete_indexed_database_record_call_count; + last_indexed_database_host = host; + last_indexed_database_name = name; + callback(JsonObject {}); + } + + virtual u64 add_indexed_database_change_listener(DevTools::TabDescription const&, OnIndexedDatabaseChange callback) const override + { + auto listener_id = next_indexed_database_change_listener_id++; + indexed_database_change_listeners.set(listener_id, move(callback)); + ++add_indexed_database_change_listener_call_count; + return listener_id; + } + + virtual void remove_indexed_database_change_listener(DevTools::TabDescription const&, u64 listener_id) const override + { + indexed_database_change_listeners.remove(listener_id); + ++remove_indexed_database_change_listener_call_count; + } + virtual void inspect_tab(DevTools::TabDescription const&, OnTabInspectionComplete callback) const override { ++inspect_tab_call_count; @@ -1245,6 +1284,13 @@ public: return fixture_session_storage_items; } + void emit_indexed_database_change(JsonObject update) const + { + VERIFY(!indexed_database_change_listeners.is_empty()); + for (auto& listener : indexed_database_change_listeners) + listener.value(update); + } + mutable Function on_dom_node_properties; mutable Function on_dom_mutation; mutable Function on_style_sheet_source; @@ -1257,6 +1303,7 @@ public: mutable Function)> on_host_cookie_change; mutable HashMap> storage_change_listeners; String tab_url { "https://example.test/"_string }; + mutable HashMap> indexed_database_change_listeners; struct NavigationListener { Function on_navigation_started; @@ -1287,6 +1334,12 @@ public: mutable bool fail_clear_storage { false }; mutable size_t inspect_indexed_database_storage_call_count { 0 }; mutable size_t inspect_indexed_database_objects_call_count { 0 }; + mutable size_t delete_indexed_database_call_count { 0 }; + mutable size_t clear_indexed_database_object_store_call_count { 0 }; + mutable size_t delete_indexed_database_record_call_count { 0 }; + mutable size_t add_indexed_database_change_listener_call_count { 0 }; + mutable size_t remove_indexed_database_change_listener_call_count { 0 }; + mutable u64 next_indexed_database_change_listener_id { 1 }; mutable size_t inspect_accessibility_tree_call_count { 0 }; mutable size_t listen_for_dom_properties_call_count { 0 }; mutable size_t stop_listening_for_dom_properties_call_count { 0 }; @@ -2549,6 +2602,85 @@ TEST_CASE(storage_indexed_database_store_objects) EXPECT_EQ(record.get_string("value"sv).value(), "{\"name\":\"Ada\"}"sv); } +TEST_CASE(storage_indexed_database_change_events) +{ + auto session = create_session(); + auto& client = *session->client; + (void)client.read_message(); + + auto indexed_database_actor = get_indexed_database_actor(client); + EXPECT_EQ(session->delegate.add_indexed_database_change_listener_call_count, 1u); + + JsonArray added; + added.must_append(indexed_database_path("fixtures (default)"sv, "people"sv)); + + JsonArray record_path; + record_path.must_append("fixtures (default)"sv); + record_path.must_append("people"sv); + record_path.must_append(1); + JsonArray changed; + changed.must_append(record_path.serialized()); + + JsonArray deleted; + deleted.must_append(indexed_database_path("empty (default)"sv)); + + JsonObject added_hosts; + added_hosts.set("https://example.test"sv, move(added)); + JsonObject added_types; + added_types.set("indexedDB"sv, move(added_hosts)); + + JsonObject changed_hosts; + changed_hosts.set("https://example.test"sv, move(changed)); + JsonObject changed_types; + changed_types.set("indexedDB"sv, move(changed_hosts)); + + JsonObject deleted_hosts; + deleted_hosts.set("https://example.test"sv, move(deleted)); + JsonObject deleted_types; + deleted_types.set("indexedDB"sv, move(deleted_hosts)); + + JsonObject update; + update.set("added"sv, move(added_types)); + update.set("changed"sv, move(changed_types)); + update.set("deleted"sv, move(deleted_types)); + session->delegate.emit_indexed_database_change(move(update)); + + auto stores_update = read_packet_with_type(client, "storesUpdate"sv); + EXPECT_EQ(stores_update.get_string("from"sv).value(), indexed_database_actor); + + auto added_paths = get_indexed_database_update_paths(stores_update, "added"sv); + EXPECT_EQ(added_paths.size(), 1u); + EXPECT_EQ(added_paths.at(0).as_string(), indexed_database_path("fixtures (default)"sv, "people"sv)); + + auto changed_paths = get_indexed_database_update_paths(stores_update, "changed"sv); + EXPECT_EQ(changed_paths.size(), 1u); + EXPECT_EQ(changed_paths.at(0).as_string(), record_path.serialized()); + + auto deleted_paths = get_indexed_database_update_paths(stores_update, "deleted"sv); + EXPECT_EQ(deleted_paths.size(), 1u); + EXPECT_EQ(deleted_paths.at(0).as_string(), indexed_database_path("empty (default)"sv)); +} + +TEST_CASE(storage_indexed_database_serializes_live_tree_updates) +{ + Web::IndexedDB::TransactionChanges changes; + changes.added.append({ "fixtures"_string, "people"_string }); + changes.added.append({ "fixtures"_string, "people"_string, JsonValue { 1 } }); + changes.changed.append({ "fixtures"_string, "people"_string, JsonValue { 2 } }); + changes.deleted.append({ "fixtures"_string, "people"_string, JsonValue { 3 } }); + + auto update = DevTools::IndexedDB::serialize_update("https://example.test/page"_string, changes); + + auto added_paths = update.get_object("added"sv) + ->get_object("indexedDB"sv) + ->get_array("https://example.test"sv) + .release_value(); + EXPECT_EQ(added_paths.size(), 1u); + EXPECT_EQ(added_paths.at(0).as_string(), indexed_database_path("fixtures (default)"sv, "people"sv)); + EXPECT(!update.get_object("changed"sv).has_value()); + EXPECT(!update.get_object("deleted"sv).has_value()); +} + TEST_CASE(storage_cookie_store_objects) { auto session = create_session();