LibWeb: Begin implementing SharedWorker
Some checks are pending
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

Shared workers are essentially just workers that may be accessed from
scripts within the same origin. There are plenty of FIXMEs here (mostly
building on existing worker FIXMEs that are already in place), but this
lets us run the shared worker variants of WPT tests.
This commit is contained in:
Timothy Flynn 2025-05-02 12:06:41 -04:00 committed by Tim Flynn
commit ea77092100
Notes: github-actions[bot] 2025-05-02 21:49:00 +00:00
12 changed files with 392 additions and 6 deletions

View file

@ -521,6 +521,8 @@ set(SOURCES
HTML/SessionHistoryTraversalQueue.cpp
HTML/ShadowRealmGlobalScope.cpp
HTML/SharedResourceRequest.cpp
HTML/SharedWorker.cpp
HTML/SharedWorkerGlobalScope.cpp
HTML/SourceSet.cpp
HTML/SourceSnapshotParams.cpp
HTML/Storage.cpp

View file

@ -562,6 +562,8 @@ class RadioNodeList;
class SelectedFile;
class SessionHistoryEntry;
class SharedResourceRequest;
class SharedWorker;
class SharedWorkerGlobalScope;
class Storage;
class SubmitEvent;
class TextMetrics;

View file

@ -0,0 +1,184 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SharedWorkerPrototype.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/EventNames.h>
#include <LibWeb/HTML/MessageEvent.h>
#include <LibWeb/HTML/MessagePort.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/SharedWorker.h>
#include <LibWeb/HTML/SharedWorkerGlobalScope.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/Worker.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(SharedWorker);
// https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker
WebIDL::ExceptionOr<GC::Ref<SharedWorker>> SharedWorker::construct_impl(JS::Realm& realm, String const& script_url, Variant<String, WorkerOptions>& options_value)
{
// FIXME: 1. Let compliantScriptURL be the result of invoking the Get Trusted Type compliant string algorithm with
// TrustedScriptURL, this's relevant global object, scriptURL, "SharedWorker constructor", and "script".
auto const& compliant_script_url = script_url;
// 2. If options is a DOMString, set options to a new WorkerOptions dictionary whose name member is set to the value
// of options and whose other members are set to their default values.
auto options = options_value.visit(
[&](String& options) {
return WorkerOptions { .name = move(options) };
},
[&](WorkerOptions& options) {
return move(options);
});
// 3. Let outside settings be the current settings object.
auto& outside_settings = current_principal_settings_object();
// 4. Let urlRecord be the result of encoding-parsing a URL given compliantScriptURL, relative to outside settings.
auto url = outside_settings.encoding_parse_url(compliant_script_url);
// 5. If urlRecord is failure, then throw a "SyntaxError" DOMException.
if (!url.has_value())
return WebIDL::SyntaxError::create(realm, "SharedWorker constructed with invalid URL"_string);
// 7. Let outside port be a new MessagePort in outside settings's realm.
// NOTE: We do this first so that we can store the port as a GC::Ref.
auto outside_port = MessagePort::create(outside_settings.realm());
// 6. Let worker be a new SharedWorker object.
// 8. Assign outside port to the port attribute of worker.
auto worker = realm.create<SharedWorker>(realm, url.release_value(), options, outside_port);
// 9. Let callerIsSecureContext be true if outside settings is a secure context; otherwise, false.
auto caller_is_secure_context = HTML::is_secure_context(outside_settings);
// 10. Let outside storage key be the result of running obtain a storage key for non-storage purposes given outside settings.
auto outside_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(outside_settings);
// 11. Enqueue the following steps to the shared worker manager:
// FIXME: "A user agent has an associated shared worker manager which is the result of starting a new parallel queue."
// We just use the singular global event loop for now.
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [worker, outside_port, &outside_settings, caller_is_secure_context, outside_storage_key = move(outside_storage_key)]() mutable {
// 1. Let worker global scope be null.
GC::Ptr<SharedWorkerGlobalScope> worker_global_scope;
// 2. For each scope in the list of all SharedWorkerGlobalScope objects:
for (auto& scope : all_shared_worker_global_scopes()) {
// 1. Let worker storage key be the result of running obtain a storage key for non-storage purposes given
// scope's relevant settings object.
auto worker_storage_key = StorageAPI::obtain_a_storage_key_for_non_storage_purposes(HTML::relevant_settings_object(scope));
// 2. If all of the following are true:
if (
// * worker storage key equals outside storage key;
worker_storage_key == outside_storage_key
// * scope's closing flag is false;
&& !scope->is_closing()
// * scope's constructor url equals urlRecord; and
&& scope->url() == worker->m_script_url
// * scope's name equals the value of options's name member,
&& scope->name() == worker->m_options.name)
// then:
{
// 1. Set worker global scope to scope.
worker_global_scope = scope;
// 2. Break.
break;
}
}
// FIXME: 3. If worker global scope is not null, but the user agent has been configured to disallow communication
// between the worker represented by the worker global scope and the scripts whose settings object is outside
// settings, then set worker global scope to null.
// FIXME: 4. If worker global scope is not null, then check if worker global scope's type and credentials match the
// options values. If not, queue a task to fire an event named error and abort these steps.
// 5. If worker global scope is not null, then run these subsubsteps:
if (worker_global_scope) {
// 1. Let settings object be the relevant settings object for worker global scope.
auto& settings_object = HTML::relevant_settings_object(*worker_global_scope);
// 2. Let workerIsSecureContext be true if settings object is a secure context; otherwise, false.
auto worker_is_secure_context = HTML::is_secure_context(settings_object);
// 3. If workerIsSecureContext is not callerIsSecureContext, then queue a task to fire an event named error
// at worker and abort these steps. [SECURE-CONTEXTS]
if (worker_is_secure_context != caller_is_secure_context) {
queue_a_task(HTML::Task::Source::Unspecified, nullptr, nullptr, GC::create_function(worker->heap(), [worker]() {
worker->dispatch_event(DOM::Event::create(worker->realm(), HTML::EventNames::error));
}));
return;
}
// FIXME: 4. Associate worker with worker global scope.
// 5. Let inside port be a new MessagePort in settings object's realm.
auto inside_port = HTML::MessagePort::create(settings_object.realm());
// 6. Entangle outside port and inside port.
outside_port->entangle_with(inside_port);
// 7. Queue a task, using the DOM manipulation task source, to fire an event named connect at worker global
// scope, using MessageEvent, with the data attribute initialized to the empty string, the ports attribute
// initialized to a new frozen array containing only inside port, and the source attribute initialized to
// inside port.
queue_a_task(HTML::Task::Source::DOMManipulation, nullptr, nullptr, GC::create_function(worker->heap(), [worker_global_scope, inside_port]() {
auto& realm = worker_global_scope->realm();
MessageEventInit init;
init.data = JS::PrimitiveString::create(realm.vm(), String {});
init.ports.append(inside_port);
init.source = inside_port;
worker_global_scope->dispatch_event(MessageEvent::create(realm, HTML::EventNames::connect, init));
}));
// FIXME: 8. Append the relevant owner to add given outside settings to worker global scope's owner set.
}
// 6. Otherwise, in parallel, run a worker given worker, urlRecord, outside settings, outside port, and options.
else {
run_a_worker(worker, worker->m_script_url, outside_settings, outside_port, worker->m_options);
}
}));
// 12. Return worker.
return worker;
}
SharedWorker::SharedWorker(JS::Realm& realm, URL::URL script_url, WorkerOptions options, MessagePort& port)
: DOM::EventTarget(realm)
, m_script_url(move(script_url))
, m_options(move(options))
, m_port(port)
{
}
SharedWorker::~SharedWorker() = default;
void SharedWorker::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SharedWorker);
Base::initialize(realm);
}
void SharedWorker::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
visitor.visit(m_port);
visitor.visit(m_agent);
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibURL/URL.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/AbstractWorker.h>
#include <LibWeb/HTML/WorkerAgentParent.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::HTML {
// https://html.spec.whatwg.org/multipage/workers.html#dedicated-workers-and-the-worker-interface
class SharedWorker final
: public DOM::EventTarget
, public HTML::AbstractWorker {
WEB_PLATFORM_OBJECT(SharedWorker, DOM::EventTarget);
GC_DECLARE_ALLOCATOR(SharedWorker);
public:
static WebIDL::ExceptionOr<GC::Ref<SharedWorker>> construct_impl(JS::Realm&, String const& script_url, Variant<String, WorkerOptions>& options);
virtual ~SharedWorker();
GC::Ref<MessagePort> port() { return m_port; }
void set_agent(WorkerAgentParent& agent) { m_agent = agent; }
private:
SharedWorker(JS::Realm&, URL::URL script_url, WorkerOptions, MessagePort&);
// ^AbstractWorker
virtual DOM::EventTarget& this_event_target() override { return *this; }
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
URL::URL m_script_url;
WorkerOptions m_options;
GC::Ref<MessagePort> m_port;
GC::Ptr<WorkerAgentParent> m_agent;
};
}

View file

@ -0,0 +1,15 @@
#import <DOM/EventTarget.idl>
#import <HTML/AbstractWorker.idl>
#import <HTML/MessagePort.idl>
#import <HTML/Worker.idl>
// https://html.spec.whatwg.org/multipage/workers.html#sharedworker
[Exposed=Window]
interface SharedWorker : EventTarget {
// FIXME: "DOMString scriptURL" should be "(TrustedScriptURL or USVString) scriptURL".
constructor(DOMString scriptURL, optional (DOMString or WorkerOptions) options = {});
readonly attribute MessagePort port;
};
SharedWorker includes AbstractWorker;

View file

@ -0,0 +1,68 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SharedWorkerExposedInterfaces.h>
#include <LibWeb/HTML/SharedWorkerGlobalScope.h>
#include <LibWeb/Page/Page.h>
namespace Web::HTML {
GC_DEFINE_ALLOCATOR(SharedWorkerGlobalScope);
HashTable<GC::RawRef<SharedWorkerGlobalScope>>& all_shared_worker_global_scopes()
{
static HashTable<GC::RawRef<SharedWorkerGlobalScope>> set;
return set;
}
SharedWorkerGlobalScope::SharedWorkerGlobalScope(JS::Realm& realm, GC::Ref<Web::Page> page, String name)
: WorkerGlobalScope(realm, page)
, m_name(move(name))
{
all_shared_worker_global_scopes().set(*this);
}
SharedWorkerGlobalScope::~SharedWorkerGlobalScope() = default;
void SharedWorkerGlobalScope::initialize_web_interfaces_impl()
{
auto& realm = this->realm();
Bindings::add_shared_worker_exposed_interfaces(*this);
SharedWorkerGlobalScopeGlobalMixin::initialize(realm, *this);
Base::initialize_web_interfaces_impl();
}
void SharedWorkerGlobalScope::finalize()
{
Base::finalize();
WindowOrWorkerGlobalScopeMixin::finalize();
all_shared_worker_global_scopes().remove(*this);
}
// https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworkerglobalscope-close
void SharedWorkerGlobalScope::close()
{
// The close() method steps are to close a worker given this.
close_a_worker();
}
#define __ENUMERATE(attribute_name, event_name) \
void SharedWorkerGlobalScope::set_##attribute_name(WebIDL::CallbackType* value) \
{ \
set_event_handler_attribute(event_name, move(value)); \
} \
\
WebIDL::CallbackType* SharedWorkerGlobalScope::attribute_name() \
{ \
return event_handler_attribute(event_name); \
}
ENUMERATE_SHARED_WORKER_GLOBAL_SCOPE_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/HashTable.h>
#include <LibJS/Forward.h>
#include <LibWeb/Bindings/SharedWorkerGlobalScopeGlobalMixin.h>
#include <LibWeb/HTML/WorkerGlobalScope.h>
namespace Web::HTML {
#define ENUMERATE_SHARED_WORKER_GLOBAL_SCOPE_EVENT_HANDLERS(E) \
E(onconnect, HTML::EventNames::connect)
class SharedWorkerGlobalScope
: public WorkerGlobalScope
, public Bindings::SharedWorkerGlobalScopeGlobalMixin {
WEB_PLATFORM_OBJECT(SharedWorkerGlobalScope, WorkerGlobalScope);
GC_DECLARE_ALLOCATOR(SharedWorkerGlobalScope);
public:
virtual ~SharedWorkerGlobalScope() override;
String const& name() const { return m_name; }
void close();
#define __ENUMERATE(attribute_name, event_name) \
void set_##attribute_name(WebIDL::CallbackType*); \
WebIDL::CallbackType* attribute_name();
ENUMERATE_SHARED_WORKER_GLOBAL_SCOPE_EVENT_HANDLERS(__ENUMERATE)
#undef __ENUMERATE
private:
SharedWorkerGlobalScope(JS::Realm&, GC::Ref<Web::Page>, String name);
virtual void initialize_web_interfaces_impl() override;
virtual void finalize() override;
String m_name;
};
HashTable<GC::RawRef<SharedWorkerGlobalScope>>& all_shared_worker_global_scopes();
}

View file

@ -0,0 +1,11 @@
#import <HTML/WorkerGlobalScope.idl>
// https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
[Global=(Worker,SharedWorker),Exposed=SharedWorker]
interface SharedWorkerGlobalScope : WorkerGlobalScope {
[Replaceable] readonly attribute DOMString name;
undefined close();
attribute EventHandler onconnect;
};

View file

@ -10,6 +10,7 @@
#include <LibWeb/HTML/MessagePort.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/WindowEnvironmentSettingsObject.h>
#include <LibWeb/HTML/SharedWorker.h>
#include <LibWeb/HTML/Worker.h>
namespace Web::HTML {
@ -82,14 +83,14 @@ WebIDL::ExceptionOr<GC::Ref<Worker>> Worker::create(String const& script_url, Wo
// 9. Run this step in parallel:
// 1. Run a worker given worker, worker URL, outside settings, outside port, and options.
worker->run_a_worker(url.value(), outside_settings, *outside_port, options);
run_a_worker(worker, url.value(), outside_settings, *outside_port, options);
// 10. Return worker
return worker;
}
// https://html.spec.whatwg.org/multipage/workers.html#run-a-worker
void Worker::run_a_worker(URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr<MessagePort> port, WorkerOptions const& options)
void run_a_worker(Variant<GC::Ref<Worker>, GC::Ref<SharedWorker>> worker, URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr<MessagePort> port, WorkerOptions const& options)
{
// 1. Let is shared be true if worker is a SharedWorker object, and false otherwise.
// FIXME: SharedWorker support
@ -107,10 +108,11 @@ void Worker::run_a_worker(URL::URL& url, EnvironmentSettingsObject& outside_sett
// 5. Let unsafeWorkerCreationTime be the unsafe shared current time.
// 6. Let agent be the result of obtaining a dedicated/shared worker agent given outside settings
// and is shared. Run the rest of these steps in that agent.
// and is shared. Run the rest of these steps in that agent.
// Note: This spawns a new process to act as the 'agent' for the worker.
m_agent = outside_settings.realm().create<WorkerAgentParent>(url, options, port, outside_settings);
auto agent = outside_settings.realm().create<WorkerAgentParent>(url, options, port, outside_settings);
worker.visit([&](auto worker) { worker->set_agent(agent); });
}
// https://html.spec.whatwg.org/multipage/workers.html#dom-worker-terminate

View file

@ -42,6 +42,8 @@ public:
GC::Ptr<MessagePort> outside_message_port() { return m_outside_port; }
void set_agent(WorkerAgentParent& agent) { m_agent = agent; }
#undef __ENUMERATE
#define __ENUMERATE(attribute_name, event_name) \
void set_##attribute_name(WebIDL::CallbackType*); \
@ -66,8 +68,8 @@ private:
GC::Ptr<MessagePort> m_outside_port;
GC::Ptr<WorkerAgentParent> m_agent;
void run_a_worker(URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr<MessagePort> outside_port, WorkerOptions const& options);
};
void run_a_worker(Variant<GC::Ref<Worker>, GC::Ref<SharedWorker>> worker, URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr<MessagePort> outside_port, WorkerOptions const& options);
}

View file

@ -234,6 +234,8 @@ libweb_js_bindings(HTML/PopStateEvent)
libweb_js_bindings(HTML/PromiseRejectionEvent)
libweb_js_bindings(HTML/RadioNodeList)
libweb_js_bindings(HTML/ShadowRealmGlobalScope GLOBAL)
libweb_js_bindings(HTML/SharedWorker)
libweb_js_bindings(HTML/SharedWorkerGlobalScope GLOBAL)
libweb_js_bindings(HTML/Storage)
libweb_js_bindings(HTML/StorageEvent)
libweb_js_bindings(HTML/SubmitEvent)

View file

@ -371,6 +371,7 @@ Set
ShadowRealm
ShadowRoot
SharedArrayBuffer
SharedWorker
SourceBuffer
SourceBufferList
StaticRange