LibWeb: Change SessionHistoryTraversalQueue to use Promises

If multiple cross-document navigations are queued on
SessionHistoryTraversalQueue, running the next entry before the current
document load is finished may result in a deadlock. If the new document
has a navigable element of its own, it will append steps to SHTQ and
hang in nested spin_until.
This change uses promises to ensure that the current document loads
before the next entry is executed.

Fixes timeouts in the imported tests.

Co-authored-by: Sam Atkins <sam@ladybird.org>
This commit is contained in:
Prajjwal 2025-07-04 10:53:28 +05:30 committed by Alexander Kalenik
parent eed4dd3745
commit 50a79c6af8
Notes: github-actions[bot] 2025-11-26 11:28:29 +00:00
20 changed files with 781 additions and 82 deletions

View file

@ -61,7 +61,7 @@ bool build_xml_document(DOM::Document& document, ByteBuffer const& data, Optiona
}
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#navigate-html
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::NavigationParams const& navigation_params)
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::NavigationParams const& navigation_params, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing)
{
// To load an HTML document, given navigation params navigationParams:
@ -74,7 +74,8 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::Navi
if (document->url_string() == "about:blank"_string
&& navigation_params.response->body()->length().value_or(0) == 0) {
TRY(document->populate_with_html_head_and_body());
// Nothing else is added to the document, so mark it as loaded.
// NB: Nothing else is added to the document, so mark it as loaded and resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
HTML::HTMLParser::the_end(document);
}
@ -92,8 +93,10 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::Navi
// causes a load event to be fired.
else {
// FIXME: Parse as we receive the document data, instead of waiting for the whole document to be fetched first.
auto process_body = GC::create_function(document->heap(), [document, url = navigation_params.response->url().value(), mime_type = navigation_params.response->header_list()->extract_mime_type()](ByteBuffer data) {
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(document->heap(), [document = document, data = move(data), url = url, mime_type] {
auto process_body = GC::create_function(document->heap(), [document, signal_to_continue_session_history_processing, url = navigation_params.response->url().value(), mime_type = navigation_params.response->header_list()->extract_mime_type()](ByteBuffer data) {
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(document->heap(), [signal_to_continue_session_history_processing, document = document, data = move(data), url = url, mime_type] {
// NB: If document is part of a session history entry's traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
auto parser = HTML::HTMLParser::create_with_uncertain_encoding(document, data, mime_type);
parser->run(url);
}));
@ -112,7 +115,7 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_html_document(HTML::Navi
}
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#read-xml
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type)
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing)
{
// When faced with displaying an XML file inline, provided navigation params navigationParams and a string type, user agents
// must follow the requirements defined in XML and Namespaces in XML, XML Media Types, DOM, and other relevant specifications
@ -147,7 +150,7 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::Navig
if (auto maybe_encoding = type.parameters().get("charset"sv); maybe_encoding.has_value())
content_encoding = maybe_encoding.value();
auto process_body = GC::create_function(document->heap(), [document, url = navigation_params.response->url().value(), content_encoding = move(content_encoding), mime = type](ByteBuffer data) {
auto process_body = GC::create_function(document->heap(), [document, signal_to_continue_session_history_processing, url = navigation_params.response->url().value(), content_encoding = move(content_encoding), mime = type](ByteBuffer data) {
Optional<TextCodec::Decoder&> decoder;
// The actual HTTP headers and other metadata, not the headers as mutated or implied by the algorithms given in this specification,
// are the ones that must be used when determining the character encoding according to the rules given in the above specifications.
@ -164,8 +167,10 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::Navig
dbgln("XML Document contains improperly-encoded characters");
convert_to_xml_error_document(document, "XML Document contains improperly-encoded characters"_utf16);
// NOTE: This ensures that the `load` event gets fired for the frame loading this document.
// NB: This ensures that the `load` event gets fired for the frame loading this document.
document->completely_finish_loading();
// NB: If document is part of a session history entry's traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
return;
}
auto source = decoder->to_utf8(data);
@ -174,10 +179,14 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::Navig
dbgln("Failed to decode XML document: {}", source.error());
convert_to_xml_error_document(document, Utf16String::formatted("Failed to decode XML document: {}", source.error()));
// NOTE: This ensures that the `load` event gets fired for the frame loading this document.
// NB: This ensures that the `load` event gets fired for the frame loading this document.
document->completely_finish_loading();
// NB: If document is part of session history traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
return;
}
// NB: If document is part of session history traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
XML::Parser parser(source.value(), { .preserve_cdata = true, .preserve_comments = true, .resolve_external_resource = resolve_xml_resource });
XMLDocumentBuilder builder { document };
auto result = parser.parse_with_listener(builder);
@ -186,7 +195,7 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::Navig
dbgln("Failed to parse XML document: {}", result.error());
convert_to_xml_error_document(document, Utf16String::formatted("Failed to parse XML document: {}", result.error()));
// NOTE: XMLDocumentBuilder ensures that the `load` event gets fired. We don't need to do anything else here.
// NB: XMLDocumentBuilder ensures that the `load` event gets fired. We don't need to do anything else here.
}
});
@ -201,7 +210,7 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_xml_document(HTML::Navig
}
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#navigate-text
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_text_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type)
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_text_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing)
{
// To load a text document, given a navigation params navigationParams and a string type:
@ -228,10 +237,12 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_text_document(HTML::Navi
// document's relevant global object to have the parser to process the implied EOF character, which eventually causes a
// load event to be fired.
// FIXME: Parse as we receive the document data, instead of waiting for the whole document to be fetched first.
auto process_body = GC::create_function(document->heap(), [document, url = navigation_params.response->url().value(), mime = type](ByteBuffer data) {
auto process_body = GC::create_function(document->heap(), [document, signal_to_continue_session_history_processing, url = navigation_params.response->url().value(), mime = type](ByteBuffer data) {
auto encoding = run_encoding_sniffing_algorithm(document, data, mime);
dbgln_if(HTML_PARSER_DEBUG, "The encoding sniffing algorithm returned encoding '{}'", encoding);
// NB: If document is part of session history traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
auto parser = HTML::HTMLParser::create_for_scripting(document);
parser->tokenizer().update_insertion_point();
@ -266,7 +277,7 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_text_document(HTML::Navi
}
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#navigate-media
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_media_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type)
static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_media_document(HTML::NavigationParams const& navigation_params, MimeSniff::MimeType type, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing)
{
// To load a media document, given navigationParams and a string type:
@ -351,7 +362,10 @@ static WebIDL::ExceptionOr<GC::Ref<DOM::Document>> load_media_document(HTML::Nav
auto& realm = document->realm();
navigation_params.response->body()->fully_read(
realm,
GC::create_function(document->heap(), [document](ByteBuffer) { HTML::HTMLParser::the_end(document); }),
GC::create_function(document->heap(), [document, signal_to_continue_session_history_processing](ByteBuffer) {
// NB: If document is part of session history traversal, resolve the signal_to_continue_session_history_processing.
signal_to_continue_session_history_processing->resolve({});
HTML::HTMLParser::the_end(document); }),
GC::create_function(document->heap(), [](JS::Value) {}),
GC::Ref { realm.global_object() });
@ -396,11 +410,13 @@ bool can_load_document_with_type(MimeSniff::MimeType const& type)
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#loading-a-document
GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_params)
GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_params, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing)
{
// To load a document given navigation params navigationParams, source snapshot params sourceSnapshotParams,
// and origin initiatorOrigin, perform the following steps. They return a Document or null.
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
// 1. Let type be the computed type of navigationParams's response.
auto supplied_type = navigation_params.response->header_list()->extract_mime_type();
auto type = MimeSniff::Resource::sniff(
@ -421,14 +437,14 @@ GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_pa
// -> an HTML MIME type
if (type.is_html()) {
// Return the result of loading an HTML document, given navigationParams.
return load_html_document(navigation_params).release_value_but_fixme_should_propagate_errors();
return load_html_document(navigation_params, signal_to_continue_session_history_processing).release_value_but_fixme_should_propagate_errors();
}
// -> an XML MIME type that is not an explicitly supported XML MIME type
// FIXME: that is not an explicitly supported XML MIME type
if (type.is_xml()) {
// Return the result of loading an XML document given navigationParams and type.
return load_xml_document(navigation_params, type).release_value_but_fixme_should_propagate_errors();
return load_xml_document(navigation_params, type, signal_to_continue_session_history_processing).release_value_but_fixme_should_propagate_errors();
}
// -> a JavaScript MIME type
@ -442,7 +458,7 @@ GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_pa
|| type.essence() == "text/plain"_string
|| type.essence() == "text/vtt"_string) {
// Return the result of loading a text document given navigationParams and type.
return load_text_document(navigation_params, type).release_value_but_fixme_should_propagate_errors();
return load_text_document(navigation_params, type, signal_to_continue_session_history_processing).release_value_but_fixme_should_propagate_errors();
}
// -> "multipart/x-mixed-replace"
@ -455,7 +471,7 @@ GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_pa
if (type.is_image()
|| type.is_audio_or_video()) {
// Return the result of loading a media document given navigationParams and type.
return load_media_document(navigation_params, type).release_value_but_fixme_should_propagate_errors();
return load_media_document(navigation_params, type, signal_to_continue_session_history_processing).release_value_but_fixme_should_propagate_errors();
}
// -> "application/pdf"

View file

@ -15,7 +15,7 @@
namespace Web {
bool build_xml_document(DOM::Document& document, ByteBuffer const& data, Optional<String> content_encoding);
GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_params);
GC::Ptr<DOM::Document> load_document(HTML::NavigationParams const& navigation_params, NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing);
bool can_load_document_with_type(MimeSniff::MimeType const&);
// https://html.spec.whatwg.org/multipage/document-lifecycle.html#read-ua-inline

View file

@ -125,7 +125,11 @@ void HTMLIFrameElement::post_connection()
if (auto navigable = content_navigable()) {
auto traversable = navigable->traversable_navigable();
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [this] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
set_content_navigable_has_session_history_entry_and_ready_for_navigation();
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}
})));

View file

@ -1325,8 +1325,9 @@ void Navigable::populate_session_history_entry_document(
SourceSnapshotParams const& source_snapshot_params,
TargetSnapshotParams const& target_snapshot_params,
UserNavigationInvolvement user_involvement,
NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing,
Optional<String> navigation_id,
Navigable::NavigationParamsVariant navigation_params,
NavigationParamsVariant navigation_params,
ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type,
bool allow_POST,
GC::Ptr<GC::Function<void()>> completion_steps)
@ -1344,21 +1345,24 @@ void Navigable::populate_session_history_entry_document(
// 3. Let documentResource be entry's document state's resource.
auto document_resource = entry->document_state()->resource();
auto received_navigation_params = GC::create_function(heap(), [this, entry, navigation_id, user_involvement, completion_steps, csp_navigation_type](NavigationParamsVariant received_navigation_params) {
auto received_navigation_params = GC::create_function(heap(), [this, entry, navigation_id, user_involvement, completion_steps, csp_navigation_type, signal_to_continue_session_history_processing](NavigationParamsVariant received_navigation_params) {
// AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
if (!active_window())
return;
// 5. Queue a global task on the navigation and traversal task source, given navigable's active window, to run these steps:
queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(heap(), [this, entry, received_navigation_params = move(received_navigation_params), navigation_id, user_involvement, completion_steps, csp_navigation_type]() mutable {
queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(heap(), [this, entry, received_navigation_params = move(received_navigation_params), navigation_id, user_involvement, completion_steps, csp_navigation_type, signal_to_continue_session_history_processing]() mutable {
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
if (has_been_destroyed())
return;
// 1. If navigable's ongoing navigation no longer equals navigationId, then run completionSteps and abort these steps.
if (navigation_id.has_value() && (!ongoing_navigation().has<String>() || ongoing_navigation().get<String>() != *navigation_id)) {
if (completion_steps)
if (completion_steps) {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
signal_to_continue_session_history_processing->resolve({});
completion_steps->function()();
}
return;
}
@ -1442,7 +1446,7 @@ void Navigable::populate_session_history_entry_document(
// 6. Otherwise, if navigationParams's response's status is not 204 and is not 205, then set entry's document state's document to the result of
// loading a document given navigationParams, sourceSnapshotParams, and entry's document state's initiator origin.
else if (auto const& response = received_navigation_params.get<GC::Ref<NavigationParams>>()->response; response->status() != 204 && response->status() != 205) {
auto document = load_document(received_navigation_params.get<GC::Ref<NavigationParams>>());
auto document = load_document(received_navigation_params.get<GC::Ref<NavigationParams>>(), signal_to_continue_session_history_processing);
entry->document_state()->set_document(document);
}
@ -1873,20 +1877,32 @@ void Navigable::begin_navigation(NavigateParams params)
// 9. Attempt to populate the history entry's document for historyEntry, given navigable, "navigate",
// sourceSnapshotParams, targetSnapshotParams, userInvolvement, navigationId, navigationParams,
// cspNavigationType, with allowPOST set to true and completionSteps set to the following step:
populate_session_history_entry_document(history_entry, source_snapshot_params, target_snapshot_params, user_involvement, navigation_id, navigation_params, csp_navigation_type, true, GC::create_function(heap(), [this, history_entry, history_handling, navigation_id, user_involvement] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
populate_session_history_entry_document(history_entry, source_snapshot_params, target_snapshot_params, user_involvement, signal_to_continue_session_history_processing, navigation_id, navigation_params, csp_navigation_type, true, GC::create_function(heap(), [this, signal_to_continue_session_history_processing, history_entry, history_handling, navigation_id, user_involvement] {
// 1. Append session history traversal steps to navigable's traversable to finalize a cross-document navigation given navigable, historyHandling, userInvolvement, and historyEntry.
traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, history_entry, history_handling, navigation_id, user_involvement] {
traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, history_entry, history_handling, navigation_id, user_involvement, signal_to_continue_session_history_processing] {
if (this->has_been_destroyed()) {
// NOTE: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
// AD-HOC: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
set_delaying_load_events(false);
return;
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}
if (this->ongoing_navigation() != navigation_id) {
// NOTE: This check is not in the spec but we should not continue navigation if ongoing navigation id has changed.
// AD-HOC: This check is not in the spec but we should not continue navigation if ongoing navigation id has changed.
set_delaying_load_events(false);
return;
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}
finalize_a_cross_document_navigation(*this, to_history_handling_behavior(history_handling), user_involvement, history_entry);
// AD-HOC: If the document isn't active or is still loading session history traversal queue will wait
// for it to load else resolve the signal_to_continue_session_history_processing.
if (history_entry->document() && (!history_entry->document()->is_active() || history_entry->document()->ready_state() != "loading")) {
signal_to_continue_session_history_processing->resolve({});
}
return signal_to_continue_session_history_processing;
}));
}));
}));
@ -1968,12 +1984,16 @@ void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehav
// 17. Append the following session history synchronous navigation steps involving navigable to traversable:
traversable->append_session_history_synchronous_navigation_steps(*this, GC::create_function(heap(), [this, traversable, history_entry, entry_to_replace, navigation_id, history_handling, user_involvement] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Finalize a same-document navigation given traversable, navigable, historyEntry, entryToReplace, historyHandling, and userInvolvement.
finalize_a_same_document_navigation(*traversable, *this, history_entry, entry_to_replace, history_handling, user_involvement);
signal_to_continue_session_history_processing->resolve({});
// FIXME: 2. Invoke WebDriver BiDi fragment navigated with navigable and a new WebDriver BiDi
// navigation status whose id is navigationId, url is url, and status is "complete".
(void)navigation_id;
return signal_to_continue_session_history_processing;
}));
}
@ -2081,7 +2101,7 @@ GC::Ptr<DOM::Document> Navigable::evaluate_javascript_url(URL::URL const& url, U
user_involvement);
// 17. Return the result of loading an HTML document given navigationParams.
return load_document(navigation_params);
return load_document(navigation_params, Core::Promise<Empty>::construct());
}
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-to-a-javascript:-url
@ -2167,7 +2187,11 @@ void Navigable::navigate_to_a_javascript_url(URL::URL const& url, HistoryHandlin
// 13. Append session history traversal steps to targetNavigable's traversable to finalize a cross-document navigation with targetNavigable, historyHandling, userInvolvement, and historyEntry.
traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, history_entry, history_handling, user_involvement] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
finalize_a_cross_document_navigation(*this, history_handling, user_involvement, history_entry);
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}
@ -2182,8 +2206,12 @@ void Navigable::reload(UserNavigationInvolvement user_involvement)
// 3. Append the following session history traversal steps to traversable:
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, user_involvement] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Apply the reload history step to traversable given userInvolvement.
traversable->apply_the_reload_history_step(user_involvement);
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}
@ -2406,10 +2434,13 @@ void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_
// 13. Append the following session history synchronous navigation steps involving navigable to traversable:
traversable->append_session_history_synchronous_navigation_steps(*navigable, GC::create_function(document.realm().heap(), [traversable, navigable, new_entry, entry_to_replace, history_handling] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Finalize a same-document navigation given traversable, navigable, newEntry, entryToReplace, historyHandling, and "none".
finalize_a_same_document_navigation(*traversable, *navigable, new_entry, entry_to_replace, history_handling, UserNavigationInvolvement::None);
signal_to_continue_session_history_processing->resolve({});
// 2. FIXME: Invoke WebDriver BiDi history updated with navigable.
return signal_to_continue_session_history_processing;
}));
}

View file

@ -124,6 +124,7 @@ public:
SourceSnapshotParams const& source_snapshot_params,
TargetSnapshotParams const& target_snapshot_params,
UserNavigationInvolvement user_involvement,
NonnullRefPtr<Core::Promise<Empty>> signal_to_continue_session_history_processing,
Optional<String> navigation_id = {},
NavigationParamsVariant navigation_params = Navigable::NullOrError {},
ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type = ContentSecurityPolicy::Directives::Directive::NavigationType::Other,

View file

@ -114,6 +114,8 @@ WebIDL::ExceptionOr<void> NavigableContainer::create_new_child_navigable(GC::Ptr
// 12. Append the following session history traversal steps to traversable:
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, navigable, parent_navigable, history_entry, after_session_history_update] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Let parentDocState be parentNavigable's active session history entry's document state.
auto parent_doc_state = parent_navigable->active_session_history_entry()->document_state();
@ -143,6 +145,8 @@ WebIDL::ExceptionOr<void> NavigableContainer::create_new_child_navigable(GC::Ptr
if (after_session_history_update) {
after_session_history_update->function()();
}
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
return {};
@ -315,8 +319,12 @@ void NavigableContainer::destroy_the_child_navigable()
// 9. Append the following session history traversal steps to traversable:
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Update for navigable creation/destruction given traversable.
traversable->update_for_navigable_creation_or_destruction();
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}));
}

View file

@ -664,6 +664,8 @@ WebIDL::ExceptionOr<NavigationResult> Navigation::perform_a_navigation_api_trave
// 12. Append the following session history traversal steps to traversable:
traversable->append_session_history_traversal_steps(GC::create_function(heap(), [key, api_method_tracker, navigable, source_snapshot_params, traversable, this] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Let navigableSHEs be the result of getting session history entries given navigable.
auto navigable_shes = navigable->get_session_history_entries();
@ -685,15 +687,18 @@ WebIDL::ExceptionOr<NavigationResult> Navigation::perform_a_navigation_api_trave
}));
// 2. Abort these steps.
return;
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}
auto target_she = *it;
// 3. If targetSHE is navigable's active session history entry, then abort these steps.
// NOTE: This can occur if a previously queued traversal already took us to this session history entry.
// In that case the previous traversal will have dealt with apiMethodTracker already.
if (target_she == navigable->active_session_history_entry())
return;
if (target_she == navigable->active_session_history_entry()) {
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}
// 4. Let result be the result of applying the traverse history step given by targetSHE's step to traversable,
// given sourceSnapshotParams, navigable, and "none".
@ -725,6 +730,8 @@ WebIDL::ExceptionOr<NavigationResult> Navigation::perform_a_navigation_api_trave
reject_the_finished_promise(api_method_tracker, WebIDL::SecurityError::create(realm, "Navigation disallowed from this origin"_utf16));
}));
}
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
// 13. Return a navigation API method tracker-derived result for apiMethodTracker.

View file

@ -12,7 +12,7 @@ namespace Web::HTML {
GC_DEFINE_ALLOCATOR(SessionHistoryTraversalQueue);
GC_DEFINE_ALLOCATOR(SessionHistoryTraversalQueueEntry);
GC::Ref<SessionHistoryTraversalQueueEntry> SessionHistoryTraversalQueueEntry::create(JS::VM& vm, GC::Ref<GC::Function<void()>> steps, GC::Ptr<HTML::Navigable> target_navigable)
GC::Ref<SessionHistoryTraversalQueueEntry> SessionHistoryTraversalQueueEntry::create(JS::VM& vm, GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps, GC::Ptr<HTML::Navigable> target_navigable)
{
return vm.heap().allocate<SessionHistoryTraversalQueueEntry>(steps, target_navigable);
}
@ -31,12 +31,20 @@ SessionHistoryTraversalQueue::SessionHistoryTraversalQueue()
m_timer->start();
return;
}
while (m_queue.size() > 0) {
if (m_current_promise && !m_current_promise->is_resolved() && !m_current_promise->is_rejected()) {
m_timer->start();
return;
}
m_is_task_running = true;
auto entry = m_queue.take_first();
entry->execute_steps();
m_current_promise = entry->execute_steps();
m_is_task_running = false;
}
m_current_promise = {};
});
}
@ -46,7 +54,7 @@ void SessionHistoryTraversalQueue::visit_edges(JS::Cell::Visitor& visitor)
visitor.visit(m_queue);
}
void SessionHistoryTraversalQueue::append(GC::Ref<GC::Function<void()>> steps)
void SessionHistoryTraversalQueue::append(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps)
{
m_queue.append(SessionHistoryTraversalQueueEntry::create(vm(), steps, nullptr));
if (!m_timer->is_active()) {
@ -54,7 +62,7 @@ void SessionHistoryTraversalQueue::append(GC::Ref<GC::Function<void()>> steps)
}
}
void SessionHistoryTraversalQueue::append_sync(GC::Ref<GC::Function<void()>> steps, GC::Ptr<Navigable> target_navigable)
void SessionHistoryTraversalQueue::append_sync(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps, GC::Ptr<Navigable> target_navigable)
{
m_queue.append(SessionHistoryTraversalQueueEntry::create(vm(), steps, target_navigable));
if (!m_timer->is_active()) {

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/Vector.h>
#include <LibCore/Promise.h>
#include <LibCore/Timer.h>
#include <LibGC/CellAllocator.h>
#include <LibGC/Function.h>
@ -23,13 +24,13 @@ struct SessionHistoryTraversalQueueEntry : public JS::Cell {
GC_DECLARE_ALLOCATOR(SessionHistoryTraversalQueueEntry);
public:
static GC::Ref<SessionHistoryTraversalQueueEntry> create(JS::VM& vm, GC::Ref<GC::Function<void()>> steps, GC::Ptr<HTML::Navigable> target_navigable);
static GC::Ref<SessionHistoryTraversalQueueEntry> create(JS::VM& vm, GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps, GC::Ptr<HTML::Navigable> target_navigable);
GC::Ptr<HTML::Navigable> target_navigable() const { return m_target_navigable; }
void execute_steps() const { m_steps->function()(); }
NonnullRefPtr<Core::Promise<Empty>> execute_steps() const { return m_steps->function()(); }
private:
SessionHistoryTraversalQueueEntry(GC::Ref<GC::Function<void()>> steps, GC::Ptr<HTML::Navigable> target_navigable)
SessionHistoryTraversalQueueEntry(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps, GC::Ptr<HTML::Navigable> target_navigable)
: m_steps(steps)
, m_target_navigable(target_navigable)
{
@ -37,7 +38,7 @@ private:
virtual void visit_edges(Cell::Visitor&) override;
GC::Ref<GC::Function<void()>> m_steps;
GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> m_steps;
GC::Ptr<HTML::Navigable> m_target_navigable;
};
@ -49,8 +50,8 @@ class WEB_API SessionHistoryTraversalQueue : public JS::Cell {
public:
SessionHistoryTraversalQueue();
void append(GC::Ref<GC::Function<void()>> steps);
void append_sync(GC::Ref<GC::Function<void()>> steps, GC::Ptr<Navigable> target_navigable);
void append(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps);
void append_sync(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps, GC::Ptr<Navigable> target_navigable);
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#sync-navigations-jump-queue
GC::Ptr<SessionHistoryTraversalQueueEntry> first_synchronous_navigation_steps_with_target_navigable_not_contained_in(HashTable<GC::Ref<Navigable>> const&);
@ -61,6 +62,7 @@ private:
Vector<GC::Ref<SessionHistoryTraversalQueueEntry>> m_queue;
RefPtr<Core::Timer> m_timer;
bool m_is_task_running { false };
WeakPtr<Core::Promise<Empty>> m_current_promise;
};
}

View file

@ -273,12 +273,15 @@ Vector<GC::Root<Navigable>> TraversableNavigable::get_all_navigables_whose_curre
// 1. Let targetEntry be the result of getting the target history entry given navigable and targetStep.
auto target_entry = navigable->get_the_target_history_entry(target_step);
// 2. If targetEntry is not navigable's current session history entry or targetEntry's document state's reload pending is true, then append navigable to results.
if (target_entry != navigable->current_session_history_entry() || target_entry->document_state()->reload_pending()) {
// 2. If targetEntry is not navigable's current session history entry or targetEntry's document state's reload
// pending is true, then append navigable to results.
// AD-HOC: We don't want to choose a navigable that has ongoing traversal.
if ((target_entry != navigable->current_session_history_entry() || target_entry->document_state()->reload_pending()) && !navigable->ongoing_navigation().has<Traversal>()) {
results.append(*navigable);
}
// 3. If targetEntry's document is navigable's document, and targetEntry's document state's reload pending is false, then extend navigablesToCheck with the child navigables of navigable.
// 3. If targetEntry's document is navigable's document, and targetEntry's document state's reload pending is
// false, then extend navigablesToCheck with the child navigables of navigable.
if (target_entry->document() == navigable->active_document() && !target_entry->document_state()->reload_pending()) {
navigables_to_check.extend(navigable->child_navigables());
}
@ -651,12 +654,23 @@ TraversableNavigable::HistoryStepResult TraversableNavigable::apply_the_history_
// queue a global task on the navigation and traversal task source given navigable's active window to
// run afterDocumentPopulated.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(this->heap(), [populated_target_entry, potentially_target_specific_source_snapshot_params, target_snapshot_params, this, allow_POST, navigable, after_document_populated = GC::create_function(this->heap(), move(after_document_populated)), user_involvement] {
navigable->populate_session_history_entry_document(populated_target_entry, *potentially_target_specific_source_snapshot_params, target_snapshot_params, user_involvement, {}, Navigable::NullOrError {}, ContentSecurityPolicy::Directives::Directive::NavigationType::Other, allow_POST, GC::create_function(this->heap(), [this, after_document_populated, populated_target_entry]() mutable {
VERIFY(active_window());
queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(this->heap(), [after_document_populated, populated_target_entry]() mutable {
after_document_populated->function()(true, populated_target_entry);
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
navigable->populate_session_history_entry_document(
populated_target_entry,
*potentially_target_specific_source_snapshot_params,
target_snapshot_params,
user_involvement,
signal_to_continue_session_history_processing,
{},
Navigable::NullOrError {},
ContentSecurityPolicy::Directives::Directive::NavigationType::Other,
allow_POST,
GC::create_function(this->heap(), [this, after_document_populated, populated_target_entry]() mutable {
VERIFY(active_window());
queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(this->heap(), [after_document_populated, populated_target_entry]() mutable {
after_document_populated->function()(true, populated_target_entry);
}));
}));
}));
}));
}
// Otherwise, run afterDocumentPopulated immediately.
@ -701,7 +715,7 @@ TraversableNavigable::HistoryStepResult TraversableNavigable::apply_the_history_
m_running_nested_apply_history_step = true;
// 4. Run steps.
entry->execute_steps();
entry->execute_steps()->await().release_value_but_fixme_should_propagate_errors();
// 5. Set traversable's running nested apply history step to false.
m_running_nested_apply_history_step = false;
@ -1153,6 +1167,8 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr<DOM:
// 4. Append the following session history traversal steps to traversable:
append_session_history_traversal_steps(GC::create_function(heap(), [this, delta, source_snapshot_params, initiator_to_check, user_involvement] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Let allSteps be the result of getting all used history steps for traversable.
auto all_steps = get_all_used_history_steps();
@ -1164,12 +1180,15 @@ void TraversableNavigable::traverse_the_history_by_delta(int delta, GC::Ptr<DOM:
// 4. If allSteps[targetStepIndex] does not exist, then abort these steps.
if (target_step_index >= all_steps.size()) {
return;
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}
// 5. Apply the traverse history step allSteps[targetStepIndex] to traversable, given sourceSnapshotParams,
// initiatorToCheck, and userInvolvement.
apply_the_traverse_history_step(all_steps[target_step_index], source_snapshot_params, initiator_to_check, user_involvement);
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}
@ -1232,6 +1251,8 @@ void TraversableNavigable::definitely_close_top_level_traversable()
// 3. Append the following session history traversal steps to traversable:
append_session_history_traversal_steps(GC::create_function(heap(), [this] {
// NB: Use Core::Promise to signal SessionHistoryTraversalQueue that it can continue to execute next entry.
auto signal_to_continue_session_history_processing = Core::Promise<Empty>::construct();
// 1. Let afterAllUnloads be an algorithm step which destroys traversable.
auto after_all_unloads = GC::create_function(heap(), [this] {
destroy_top_level_traversable();
@ -1239,6 +1260,8 @@ void TraversableNavigable::definitely_close_top_level_traversable()
// 2. Unload a document and its descendants given traversable's active document, null, and afterAllUnloads.
active_document()->unload_a_document_and_its_descendants({}, after_all_unloads);
signal_to_continue_session_history_processing->resolve({});
return signal_to_continue_session_history_processing;
}));
}

View file

@ -87,12 +87,12 @@ public:
void definitely_close_top_level_traversable();
void destroy_top_level_traversable();
void append_session_history_traversal_steps(GC::Ref<GC::Function<void()>> steps)
void append_session_history_traversal_steps(GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps)
{
m_session_history_traversal_queue->append(steps);
}
void append_session_history_synchronous_navigation_steps(GC::Ref<Navigable> target_navigable, GC::Ref<GC::Function<void()>> steps)
void append_session_history_synchronous_navigation_steps(GC::Ref<Navigable> target_navigable, GC::Ref<GC::Function<NonnullRefPtr<Core::Promise<Empty>>()>> steps)
{
m_session_history_traversal_queue->append_sync(steps, target_navigable);
}