LibWebView: Add a UI session history model

Add a browser-side model for top-level history entries and history step
coordinates. This gives the UI process a structure to mirror WebContent
history across process swaps.

Add debug dumping support alongside the model so traversal state can be
inspected while working on back and forward behavior.
This commit is contained in:
Andreas Kling 2026-06-13 16:06:01 +02:00 committed by Andreas Kling
parent 9e2ec2dc5c
commit 78bb7b8d45
7 changed files with 1640 additions and 0 deletions

View file

@ -9,6 +9,7 @@ set(SOURCES
DOMNodeProperties.cpp
FileDownloader.cpp
HeadlessWebView.cpp
HistoryDebug.cpp
HistoryStore.cpp
HelperProcess.cpp
HSTSStore.cpp
@ -20,6 +21,7 @@ set(SOURCES
ProcessManager.cpp
ProcessMonitor.cpp
SearchEngine.cpp
SessionHistory.cpp
Settings.cpp
SiteIsolation.cpp
SourceHighlighter.cpp

View file

@ -24,6 +24,7 @@ class Menu;
class OutOfProcessWebView;
class ProcessManager;
class Settings;
class TraversableSessionHistory;
class ViewImplementation;
class WebContentClient;
class WebWorkerClient;

View file

@ -0,0 +1,219 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringBuilder.h>
#include <LibCore/Environment.h>
#include <LibWebView/HistoryDebug.h>
#include <LibWebView/SessionHistory.h>
namespace WebView {
bool history_debug_enabled()
{
static auto enabled = WEBVIEW_HISTORY_DEBUG || Core::Environment::has("LADYBIRD_SESSION_HISTORY_DEBUG"sv);
return enabled;
}
static void append_history_log_entry(StringBuilder& builder, Web::HTML::SessionHistoryEntryDescriptor const& entry);
static StringView document_state_resource_type(Variant<Empty, String, Web::HTML::POSTResource> const& resource)
{
if (resource.has<String>())
return "string"sv;
if (resource.has<Web::HTML::POSTResource>())
return "post"sv;
return "none"sv;
}
static void append_history_log_nested_histories(StringBuilder& builder, Vector<Web::HTML::SessionHistoryNestedHistoryDescriptor> const& nested_histories)
{
if (nested_histories.is_empty())
return;
builder.append(" nested={"sv);
for (size_t i = 0; i < nested_histories.size(); ++i) {
if (i != 0)
builder.append(", "sv);
builder.appendff("{}=[", nested_histories[i].id);
for (size_t j = 0; j < nested_histories[i].entries.size(); ++j) {
if (j != 0)
builder.append(", "sv);
append_history_log_entry(builder, nested_histories[i].entries[j]);
}
builder.append(']');
}
builder.append('}');
}
static void append_history_log_entry(StringBuilder& builder, Web::HTML::SessionHistoryEntryDescriptor const& entry)
{
builder.appendff("{}:{}", entry.step, entry.url);
auto resource_type = document_state_resource_type(entry.document_state.resource);
if (entry.document_state.id != 0
|| entry.document_state.reload_pending
|| !entry.document_state.navigable_target_name.is_empty()
|| resource_type != "none"sv) {
builder.appendff(" document_state={{id={}", entry.document_state.id);
if (resource_type != "none"sv)
builder.appendff(", resource={}", resource_type);
if (entry.document_state.reload_pending)
builder.append(", reload_pending=true"sv);
if (!entry.document_state.navigable_target_name.is_empty())
builder.appendff(", target_name={}", entry.document_state.navigable_target_name);
builder.append('}');
}
if (entry.scroll_position_data.viewport_scroll_position.has_value()) {
auto const& viewport_scroll_position = *entry.scroll_position_data.viewport_scroll_position;
builder.appendff(" scroll={{viewport=({}, {})}}", viewport_scroll_position.x(), viewport_scroll_position.y());
}
append_history_log_nested_histories(builder, entry.document_state.nested_histories);
}
static StringView scroll_restoration_mode_to_string(Web::HTML::ScrollRestorationMode mode)
{
switch (mode) {
case Web::HTML::ScrollRestorationMode::Auto:
return "auto"sv;
case Web::HTML::ScrollRestorationMode::Manual:
return "manual"sv;
}
VERIFY_NOT_REACHED();
}
static JsonObject history_json_scroll_position_data(Web::HTML::SessionHistoryEntryScrollPositionData const& scroll_position_data)
{
JsonObject serialized;
if (scroll_position_data.viewport_scroll_position.has_value()) {
auto const& viewport_scroll_position = *scroll_position_data.viewport_scroll_position;
JsonArray serialized_viewport_scroll_position;
serialized_viewport_scroll_position.must_append(viewport_scroll_position.x().to_double());
serialized_viewport_scroll_position.must_append(viewport_scroll_position.y().to_double());
serialized.set("viewport"sv, move(serialized_viewport_scroll_position));
}
return serialized;
}
static JsonArray history_json_nested_histories(Vector<Web::HTML::SessionHistoryNestedHistoryDescriptor> const& nested_histories)
{
JsonArray serialized_nested_histories;
serialized_nested_histories.ensure_capacity(nested_histories.size());
for (auto const& nested_history : nested_histories) {
JsonArray serialized_entries;
serialized_entries.ensure_capacity(nested_history.entries.size());
for (auto const& entry : nested_history.entries)
serialized_entries.must_append(history_json_entry(entry));
JsonObject serialized_nested_history;
serialized_nested_history.set("id"sv, nested_history.id);
serialized_nested_history.set("entries"sv, move(serialized_entries));
serialized_nested_histories.must_append(move(serialized_nested_history));
}
return serialized_nested_histories;
}
JsonObject history_json_entry(Web::HTML::SessionHistoryEntryDescriptor const& entry, bool current)
{
JsonObject serialized;
serialized.set("step"sv, entry.step);
serialized.set("url"sv, entry.url.serialize());
serialized.set("documentStateId"sv, entry.document_state.id);
serialized.set("resource"sv, document_state_resource_type(entry.document_state.resource));
serialized.set("reloadPending"sv, entry.document_state.reload_pending);
serialized.set("targetName"sv, entry.document_state.navigable_target_name);
serialized.set("scrollRestoration"sv, scroll_restoration_mode_to_string(entry.scroll_restoration_mode));
serialized.set("scrollPosition"sv, history_json_scroll_position_data(entry.scroll_position_data));
serialized.set("nestedHistories"sv, history_json_nested_histories(entry.document_state.nested_histories));
serialized.set("current"sv, current);
return serialized;
}
JsonArray history_json_entries(TraversableSessionHistory const& history)
{
return history_json_entries(history.entries(), history.current_top_level_entry_index());
}
JsonArray history_json_entries(Vector<Web::HTML::SessionHistoryEntryDescriptor> const& entries, Optional<size_t> current_entry_index)
{
JsonArray serialized_entries;
serialized_entries.ensure_capacity(entries.size());
for (size_t i = 0; i < entries.size(); ++i)
serialized_entries.must_append(history_json_entry(entries[i], current_entry_index.has_value() && *current_entry_index == i));
return serialized_entries;
}
JsonArray history_json_steps(TraversableSessionHistory const& history)
{
return history_json_steps(history.used_steps(), history.current_used_step_index());
}
JsonArray history_json_steps(Vector<i32> const& steps, Optional<size_t> current_step_index)
{
JsonArray serialized_steps;
serialized_steps.ensure_capacity(steps.size());
for (size_t i = 0; i < steps.size(); ++i) {
JsonObject serialized_step;
serialized_step.set("step"sv, steps[i]);
serialized_step.set("current"sv, current_step_index.has_value() && *current_step_index == i);
serialized_steps.must_append(move(serialized_step));
}
return serialized_steps;
}
ByteString history_log_entries(TraversableSessionHistory const& history)
{
StringBuilder builder;
builder.append("entries=["sv);
for (size_t i = 0; i < history.size(); ++i) {
if (i != 0)
builder.append(", "sv);
auto const* entry = history.entry_at(i);
VERIFY(entry);
if (auto const* current_entry = history.current_entry(); current_entry == entry)
builder.append("*"sv);
builder.appendff("{}:", i);
append_history_log_entry(builder, *entry);
}
builder.append("] used_steps="sv);
builder.append(history_log_steps(history.used_steps(), history.current_used_step_index()));
return builder.to_byte_string();
}
ByteString history_log_entries(Vector<Web::HTML::SessionHistoryEntryDescriptor> const& entries, Optional<size_t> current_entry_index)
{
StringBuilder builder;
builder.append('[');
for (size_t i = 0; i < entries.size(); ++i) {
if (i != 0)
builder.append(", "sv);
if (current_entry_index.has_value() && *current_entry_index == i)
builder.append("*"sv);
builder.appendff("{}:", i);
append_history_log_entry(builder, entries[i]);
}
builder.append(']');
return builder.to_byte_string();
}
ByteString history_log_steps(Vector<i32> const& steps, Optional<size_t> current_step_index)
{
StringBuilder builder;
builder.append('[');
for (size_t i = 0; i < steps.size(); ++i) {
if (i != 0)
builder.append(", "sv);
if (current_step_index.has_value() && *current_step_index == i)
builder.append("*"sv);
builder.appendff("{}:{}", i, steps[i]);
}
builder.append(']');
return builder.to_byte_string();
}
}

View file

@ -7,8 +7,14 @@
#pragma once
#include <AK/ByteString.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/Optional.h>
#include <AK/String.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWebView/Forward.h>
namespace WebView {
@ -17,4 +23,14 @@ inline ByteString history_log_suggestions(Vector<String> const& suggestions)
return ByteString::formatted("[{}]", ByteString::join(", "sv, suggestions));
}
WEBVIEW_API bool history_debug_enabled();
WEBVIEW_API ByteString history_log_entries(TraversableSessionHistory const&);
WEBVIEW_API ByteString history_log_entries(Vector<Web::HTML::SessionHistoryEntryDescriptor> const&, Optional<size_t> current_entry_index = {});
WEBVIEW_API ByteString history_log_steps(Vector<i32> const&, Optional<size_t> current_step_index = {});
WEBVIEW_API JsonObject history_json_entry(Web::HTML::SessionHistoryEntryDescriptor const&, bool current = false);
WEBVIEW_API JsonArray history_json_entries(TraversableSessionHistory const&);
WEBVIEW_API JsonArray history_json_entries(Vector<Web::HTML::SessionHistoryEntryDescriptor> const&, Optional<size_t> current_entry_index = {});
WEBVIEW_API JsonArray history_json_steps(TraversableSessionHistory const&);
WEBVIEW_API JsonArray history_json_steps(Vector<i32> const&, Optional<size_t> current_step_index = {});
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,116 @@
/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Optional.h>
#include <AK/Vector.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWebView/Export.h>
namespace WebView {
// AD-HOC: The HTML Standard stores a traversable navigable's session history entries on the traversable. Ladybird
// keeps an IPC-serializable mirror in the UI process so browser history survives WebContent process swaps
// and crash recovery. The mirror still uses the spec's session history entry and all used history steps model.
//
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-session-history-entries
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps
class WEBVIEW_API TraversableSessionHistory {
public:
using Entry = Web::HTML::SessionHistoryEntryDescriptor;
struct TraversalTarget {
size_t target_step_index { 0 };
i32 target_step { 0 };
Entry const* target_top_level_entry { nullptr };
bool target_step_is_top_level_entry { false };
bool changes_top_level_entry { false };
};
enum class UpdateResult {
// WebContent sent the same complete top-level traversable session
// history that the UI process stores authoritatively.
CompleteSnapshot,
// WebContent sent a valid partial view of session history. The UI
// process merged it into the authoritative history mirror, but
// WebContent cannot be assumed to know or use every UI history step.
MergedPartialSnapshot,
// WebContent sent a snapshot that cannot describe the current UI-owned
// traversable session history.
InvalidSnapshot,
};
bool is_empty() const { return m_entries.is_empty(); }
size_t size() const { return m_entries.size(); }
size_t used_step_count() const { return m_used_steps.size(); }
Optional<size_t> current_used_step_index() const { return m_current_used_step_index; }
Optional<size_t> current_top_level_entry_index() const;
void clear();
void navigate(URL::URL);
void navigate(URL::URL, Variant<Empty, String, Web::HTML::POSTResource>);
void replace_current_entry_url(URL::URL);
void replace_current_entry(URL::URL, Variant<Empty, String, Web::HTML::POSTResource>);
void mark_current_entry_reload_pending();
void clear_current_entry_reload_pending();
UpdateResult update_from_web_content(Vector<Entry> entries, Vector<i32> used_steps, size_t current_used_step_index);
[[nodiscard]] bool did_seed_web_content_from_ui_process(Vector<Entry> entries, Vector<i32> used_steps, size_t current_used_step_index);
void did_seed_web_content_from_ui_process(size_t current_top_level_entry_index);
[[nodiscard]] bool did_restore_web_content_to_current_step(i32 step);
[[nodiscard]] bool did_apply_web_content_traversal_to_step(i32 step);
void forget_web_content_state();
Vector<Entry> entries() const;
Vector<i32> used_steps() const;
Vector<Entry> web_content_known_entries() const;
Vector<i32> web_content_known_used_steps() const;
Optional<i32> web_content_current_step() const;
bool web_content_uses_ui_step_coordinates() const { return m_web_content_uses_ui_step_coordinates; }
bool web_content_history_matches_mirror() const;
[[nodiscard]] bool can_go_back() const;
[[nodiscard]] bool can_go_forward() const;
[[nodiscard]] bool has_only_top_level_used_steps() const;
[[nodiscard]] bool current_step_is_top_level_entry() const;
[[nodiscard]] Optional<i32> current_step_to_restore_after_loading_top_level_entry() const;
[[nodiscard]] bool web_content_can_traverse_to(TraversalTarget const&) const;
[[nodiscard]] Optional<TraversalTarget> traversal_target_for_delta(int delta) const;
[[nodiscard]] Optional<TraversalTarget> traversal_target_for_step(i32 step) const;
[[nodiscard]] Optional<size_t> target_step_index_for_delta(int delta) const;
[[nodiscard]] Optional<i32> step_at(size_t index) const;
[[nodiscard]] Entry const* current_entry() const;
[[nodiscard]] Entry const* entry_at(size_t index) const;
[[nodiscard]] Entry const* entry_for_step(i32 step) const;
[[nodiscard]] Entry const* top_level_entry_for_step(i32 step) const;
void traverse_to(size_t index);
private:
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-session-history-entries
Vector<Entry> m_entries;
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-all-used-history-steps
Vector<i32> m_used_steps;
// Index of the current session history step within m_used_steps.
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-current-session-history-step
Optional<size_t> m_current_used_step_index;
// WebContent's latest current session history step, translated into the
// UI-owned traversable session history's step coordinate space.
// https://html.spec.whatwg.org/multipage/document-sequences.html#tn-current-session-history-step
Vector<Entry> m_web_content_known_entries;
Vector<i32> m_web_content_known_used_steps;
Optional<i32> m_web_content_current_step;
// False when a partial snapshot was translated into the UI-owned step
// coordinate space. In that state WebContent still uses its original step
// numbers, so the UI must reseed/load instead of delegating traversal by step.
bool m_web_content_uses_ui_step_coordinates { false };
};
}

View file

@ -32,6 +32,7 @@ set(IMAGE_DECODER_DEBUG ON)
set(IMAGE_LOADER_DEBUG ON)
set(JS_BYTECODE_DEBUG ON)
set(JS_MODULE_DEBUG ON)
set(LADYBIRD_SESSION_HISTORY_DEBUG ON)
set(LEXER_DEBUG ON)
set(WEBVIEW_HISTORY_DEBUG ON)
set(LIBWEB_CSS_ANIMATION_DEBUG ON)