LibJS+LibWeb: Set [[CanBlock]] false to Agent for window agent

similar-origin window agents have the [[CanBlock]] flag set to false.
Achieve this by hooking up JS's concept with an agent to HTML::Agent.
For now, this is only hooked up to the similar-origin window agent
case but should be extended to the other agent types in the future.
This commit is contained in:
Shannon Booth 2025-04-20 16:49:34 +12:00 committed by Tim Flynn
parent 4cd186f3f5
commit e124ef52ee
Notes: github-actions[bot] 2025-04-22 15:52:53 +00:00
16 changed files with 106 additions and 44 deletions

View file

@ -154,6 +154,7 @@ namespace JS {
class ASTNode;
class Accessor;
class Agent;
struct AsyncGeneratorRequest;
class BigInt;
class BoundFunction;

View file

@ -1,19 +1,28 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Runtime/Agent.h>
#include <LibJS/Runtime/VM.h>
namespace JS {
Agent::~Agent() = default;
// 9.7.2 AgentCanSuspend ( ), https://tc39.es/ecma262/#sec-agentcansuspend
bool agent_can_suspend()
bool agent_can_suspend(VM const& vm)
{
// FIXME: 1. Let AR be the Agent Record of the surrounding agent.
// FIXME: 2. Return AR.[[CanBlock]].
// 1. Let AR be the Agent Record of the surrounding agent.
auto const* agent = vm.agent();
// 2. Return AR.[[CanBlock]].
// NOTE: We default to true if no agent has been provided (standalone LibJS with no embedder).
if (!agent)
return true;
return agent->can_block();
}
}

View file

@ -1,13 +1,29 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGC/Function.h>
#include <LibGC/Root.h>
#include <LibJS/Forward.h>
namespace JS {
bool agent_can_suspend();
// https://tc39.es/ecma262/#sec-agents
class Agent {
public:
virtual ~Agent();
// [[CanBlock]]
virtual bool can_block() const = 0;
virtual void spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition) = 0;
};
bool agent_can_suspend(VM const&);
}

View file

@ -186,7 +186,7 @@ static ThrowCompletionOr<Value> do_wait(VM& vm, WaitMode mode, TypedArrayBase& t
timeout = max(timeout_number.as_double(), 0.0);
// 10. If mode is sync and AgentCanSuspend() is false, throw a TypeError exception.
if (mode == WaitMode::Sync && !agent_can_suspend())
if (mode == WaitMode::Sync && !agent_can_suspend(vm))
return vm.throw_completion<TypeError>(ErrorType::AgentCannotSuspend);
// FIXME: Implement the remaining steps when we support SharedArrayBuffer.

View file

@ -6,6 +6,7 @@
*/
#include <AK/TypeCasts.h>
#include <LibJS/Runtime/Agent.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/Promise.h>
@ -98,9 +99,9 @@ ThrowCompletionOr<Value> await(VM& vm, Value value)
// FIXME: Since we don't support context suspension, we attempt to "wait" for the promise to resolve
// by syncronously running all queued promise jobs.
if (auto* custom_data = vm.custom_data()) {
if (auto* agent = vm.agent()) {
// Embedder case (i.e. LibWeb). Runs all promise jobs by performing a microtask checkpoint.
custom_data->spin_event_loop_until(GC::create_function(vm.heap(), [success] {
agent->spin_event_loop_until(GC::create_function(vm.heap(), [success] {
return success.has_value();
}));
} else {

View file

@ -37,12 +37,12 @@
namespace JS {
ErrorOr<NonnullRefPtr<VM>> VM::create(OwnPtr<CustomData> custom_data)
ErrorOr<NonnullRefPtr<VM>> VM::create(OwnPtr<Agent> agent)
{
ErrorMessages error_messages {};
error_messages[to_underlying(ErrorMessage::OutOfMemory)] = ErrorType::OutOfMemory.message();
auto vm = adopt_ref(*new VM(move(custom_data), move(error_messages)));
auto vm = adopt_ref(*new VM(move(agent), move(error_messages)));
WellKnownSymbols well_known_symbols {
#define __JS_ENUMERATE(SymbolName, snake_name) \
@ -63,12 +63,12 @@ static constexpr auto make_single_ascii_character_strings(IndexSequence<code_poi
static constexpr auto single_ascii_character_strings = make_single_ascii_character_strings(MakeIndexSequence<128>());
VM::VM(OwnPtr<CustomData> custom_data, ErrorMessages error_messages)
VM::VM(OwnPtr<Agent> agent, ErrorMessages error_messages)
: m_heap(this, [this](HashMap<GC::Cell*, GC::HeapRoot>& roots) {
gather_roots(roots);
})
, m_error_messages(move(error_messages))
, m_custom_data(move(custom_data))
, m_agent(move(agent))
{
m_bytecode_interpreter = make<Bytecode::Interpreter>(*this);

View file

@ -21,6 +21,7 @@
#include <LibGC/RootVector.h>
#include <LibJS/CyclicModule.h>
#include <LibJS/ModuleLoading.h>
#include <LibJS/Runtime/Agent.h>
#include <LibJS/Runtime/CommonPropertyNames.h>
#include <LibJS/Runtime/Completion.h>
#include <LibJS/Runtime/Error.h>
@ -46,13 +47,7 @@ enum class EvalMode {
class VM : public RefCounted<VM> {
public:
struct CustomData {
virtual ~CustomData() = default;
virtual void spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition) = 0;
};
static ErrorOr<NonnullRefPtr<VM>> create(OwnPtr<CustomData> = {});
static ErrorOr<NonnullRefPtr<VM>> create(OwnPtr<Agent> = {});
~VM();
GC::Heap& heap() { return m_heap; }
@ -245,7 +240,8 @@ public:
Function<void(Promise&)> on_promise_rejection_handled;
Function<void(Object const&, PropertyKey const&)> on_unimplemented_property_access;
CustomData* custom_data() { return m_custom_data; }
Agent* agent() { return m_agent; }
Agent const* agent() const { return m_agent; }
void save_execution_context_stack();
void clear_execution_context_stack();
@ -294,7 +290,7 @@ private:
#undef __JS_ENUMERATE
};
VM(OwnPtr<CustomData>, ErrorMessages);
VM(OwnPtr<Agent>, ErrorMessages);
void load_imported_module(ImportedModuleReferrer, ModuleRequest const&, GC::Ptr<GraphLoadingState::HostDefined>, ImportedModulePayload);
ThrowCompletionOr<void> link_and_eval_module(CyclicModule&);
@ -339,7 +335,7 @@ private:
u32 m_execution_generation { 0 };
OwnPtr<CustomData> m_custom_data;
OwnPtr<Agent> m_agent;
OwnPtr<Bytecode::Interpreter> m_bytecode_interpreter;

View file

@ -76,10 +76,10 @@ ErrorOr<void> initialize_main_thread_vm(HTML::EventLoop::Type type)
{
VERIFY(!s_main_thread_vm);
s_main_thread_vm = TRY(JS::VM::create(make<WebEngineCustomData>()));
s_main_thread_vm = TRY(JS::VM::create(make<HTML::Agent>()));
auto& custom_data = as<WebEngineCustomData>(*s_main_thread_vm->custom_data());
custom_data.agent.event_loop = s_main_thread_vm->heap().allocate<HTML::EventLoop>(type);
auto& agent = as<HTML::Agent>(*s_main_thread_vm->agent());
agent.event_loop = s_main_thread_vm->heap().allocate<HTML::EventLoop>(type);
s_main_thread_vm->on_unimplemented_property_access = [](auto const& object, auto const& property_key) {
dbgln("FIXME: Unimplemented IDL interface: '{}.{}'", object.class_name(), property_key.to_string());
@ -658,7 +658,7 @@ JS::VM& main_thread_vm()
void queue_mutation_observer_microtask(DOM::Document const& document)
{
auto& vm = main_thread_vm();
auto& surrounding_agent = as<WebEngineCustomData>(*vm.custom_data()).agent;
auto& surrounding_agent = as<HTML::Agent>(*vm.agent());
// 1. If the surrounding agents mutation observer microtask queued is true, then return.
if (surrounding_agent.mutation_observer_microtask_queued)
@ -750,11 +750,6 @@ NonnullOwnPtr<JS::ExecutionContext> create_a_new_javascript_realm(JS::VM& vm, Fu
return realm_execution_context;
}
void WebEngineCustomData::spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition)
{
Platform::EventLoopPlugin::the().spin_until(move(goal_condition));
}
// https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions
void invoke_custom_element_reactions(Vector<GC::Root<DOM::Element>>& element_queue)
{

View file

@ -17,14 +17,6 @@
namespace Web::Bindings {
struct WebEngineCustomData final : public JS::VM::CustomData {
virtual ~WebEngineCustomData() override = default;
virtual void spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition) override;
HTML::Agent agent;
};
struct WebEngineCustomJobCallbackData final : public JS::JobCallback::CustomData {
WebEngineCustomJobCallbackData(JS::Realm& incumbent_realm, OwnPtr<JS::ExecutionContext> active_script_context)
: incumbent_realm(incumbent_realm)

View file

@ -68,7 +68,7 @@ void EventLoop::schedule()
EventLoop& main_thread_event_loop()
{
return *static_cast<Bindings::WebEngineCustomData*>(Bindings::main_thread_vm().custom_data())->agent.event_loop;
return *static_cast<HTML::Agent*>(Bindings::main_thread_vm().agent())->event_loop;
}
// https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop

View file

@ -7,15 +7,27 @@
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/HTML/Scripting/Agent.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
namespace Web::HTML {
bool Agent::can_block() const
{
// similar-origin window agents can not block, see: https://html.spec.whatwg.org/multipage/webappapis.html#obtain-similar-origin-window-agent
return false;
}
void Agent::spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition)
{
Platform::EventLoopPlugin::the().spin_until(move(goal_condition));
}
// https://html.spec.whatwg.org/multipage/webappapis.html#relevant-agent
Agent& relevant_agent(JS::Object const& object)
{
// The relevant agent for a platform object platformObject is platformObject's relevant Realm's agent.
// Spec Note: This pointer is not yet defined in the JavaScript specification; see tc39/ecma262#1357.
return static_cast<Bindings::WebEngineCustomData*>(relevant_realm(object).vm().custom_data())->agent;
return *static_cast<Agent*>(relevant_realm(object).vm().agent());
}
}

View file

@ -10,6 +10,7 @@
#include <AK/Vector.h>
#include <LibGC/Root.h>
#include <LibJS/Forward.h>
#include <LibJS/Runtime/Agent.h>
#include <LibWeb/DOM/MutationObserver.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/CustomElements/CustomElementReactionsStack.h>
@ -17,7 +18,7 @@
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/webappapis.html#similar-origin-window-agent
struct Agent {
struct Agent : public JS::Agent {
GC::Root<HTML::EventLoop> event_loop;
// FIXME: These should only be on similar-origin window agents, but we don't currently differentiate agent types.
@ -40,6 +41,11 @@ struct Agent {
// A similar-origin window agent's current element queue is the element queue at the top of its custom element reactions stack.
Vector<GC::Root<DOM::Element>>& current_element_queue() { return custom_element_reactions_stack.element_queue_stack.last(); }
Vector<GC::Root<DOM::Element>> const& current_element_queue() const { return custom_element_reactions_stack.element_queue_stack.last(); }
// [[CanBlock]]
virtual bool can_block() const override;
virtual void spin_event_loop_until(GC::Root<GC::Function<bool()>> goal_condition) override;
};
Agent& relevant_agent(JS::Object const&);

View file

@ -373,7 +373,9 @@ static ErrorOr<TestMetadata, String> extract_metadata(StringView source)
metadata.harness_files.append(async_include);
metadata.is_async = true;
} else if (flag == "CanBlockIsFalse"sv) {
if (JS::agent_can_suspend())
// NOTE: This should only be skipped if AgentCanSuspend is set to true. This is currently always the case.
// Ideally we would check that, but we don't have the VM by this stage. So for now, we rely on that
// assumption.
metadata.skip_test = SkipTest::Yes;
}
}

View file

@ -0,0 +1,6 @@
Harness status: OK
Found 1 tests
1 Pass
Pass [[CanBlock]] in a Window

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../../../../resources/testharness.js"></script>
<script src="../../../../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../../../../html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.js"></script>

View file

@ -0,0 +1,11 @@
// META: global=window,serviceworker
test(() => {
// See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
const sab = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer;
const ta = new Int32Array(sab);
assert_throws_js(TypeError, () => {
Atomics.wait(ta, 0, 0, 10);
});
}, `[[CanBlock]] in a ${self.constructor.name}`);