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();