From ea7709210054c858465298d8e558e98955d06383 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 2 May 2025 12:06:41 -0400 Subject: [PATCH] LibWeb: Begin implementing SharedWorker 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. --- Libraries/LibWeb/CMakeLists.txt | 2 + Libraries/LibWeb/Forward.h | 2 + Libraries/LibWeb/HTML/SharedWorker.cpp | 184 ++++++++++++++++++ Libraries/LibWeb/HTML/SharedWorker.h | 48 +++++ Libraries/LibWeb/HTML/SharedWorker.idl | 15 ++ .../LibWeb/HTML/SharedWorkerGlobalScope.cpp | 68 +++++++ .../LibWeb/HTML/SharedWorkerGlobalScope.h | 49 +++++ .../LibWeb/HTML/SharedWorkerGlobalScope.idl | 11 ++ Libraries/LibWeb/HTML/Worker.cpp | 10 +- Libraries/LibWeb/HTML/Worker.h | 6 +- Libraries/LibWeb/idl_files.cmake | 2 + .../Text/expected/all-window-properties.txt | 1 + 12 files changed, 392 insertions(+), 6 deletions(-) create mode 100644 Libraries/LibWeb/HTML/SharedWorker.cpp create mode 100644 Libraries/LibWeb/HTML/SharedWorker.h create mode 100644 Libraries/LibWeb/HTML/SharedWorker.idl create mode 100644 Libraries/LibWeb/HTML/SharedWorkerGlobalScope.cpp create mode 100644 Libraries/LibWeb/HTML/SharedWorkerGlobalScope.h create mode 100644 Libraries/LibWeb/HTML/SharedWorkerGlobalScope.idl diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 375f746c1bc..9a67775d2fc 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 7dfd13b08a1..52619913ad3 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -562,6 +562,8 @@ class RadioNodeList; class SelectedFile; class SessionHistoryEntry; class SharedResourceRequest; +class SharedWorker; +class SharedWorkerGlobalScope; class Storage; class SubmitEvent; class TextMetrics; diff --git a/Libraries/LibWeb/HTML/SharedWorker.cpp b/Libraries/LibWeb/HTML/SharedWorker.cpp new file mode 100644 index 00000000000..402aff770d1 --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorker.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +GC_DEFINE_ALLOCATOR(SharedWorker); + +// https://html.spec.whatwg.org/multipage/workers.html#dom-sharedworker +WebIDL::ExceptionOr> SharedWorker::construct_impl(JS::Realm& realm, String const& script_url, Variant& 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(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 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); +} + +} diff --git a/Libraries/LibWeb/HTML/SharedWorker.h b/Libraries/LibWeb/HTML/SharedWorker.h new file mode 100644 index 00000000000..1cb7affb7e9 --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorker.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +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> construct_impl(JS::Realm&, String const& script_url, Variant& options); + + virtual ~SharedWorker(); + + GC::Ref 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 m_port; + GC::Ptr m_agent; +}; + +} diff --git a/Libraries/LibWeb/HTML/SharedWorker.idl b/Libraries/LibWeb/HTML/SharedWorker.idl new file mode 100644 index 00000000000..5f4f8fc1c43 --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorker.idl @@ -0,0 +1,15 @@ +#import +#import +#import +#import + +// 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; diff --git a/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.cpp b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.cpp new file mode 100644 index 00000000000..e3953ce30f4 --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +namespace Web::HTML { + +GC_DEFINE_ALLOCATOR(SharedWorkerGlobalScope); + +HashTable>& all_shared_worker_global_scopes() +{ + static HashTable> set; + return set; +} + +SharedWorkerGlobalScope::SharedWorkerGlobalScope(JS::Realm& realm, GC::Ref 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 + +} diff --git a/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.h b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.h new file mode 100644 index 00000000000..5e35a5e2d9b --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.h @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include + +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, String name); + + virtual void initialize_web_interfaces_impl() override; + virtual void finalize() override; + + String m_name; +}; + +HashTable>& all_shared_worker_global_scopes(); + +} diff --git a/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.idl b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.idl new file mode 100644 index 00000000000..06cc23fc02c --- /dev/null +++ b/Libraries/LibWeb/HTML/SharedWorkerGlobalScope.idl @@ -0,0 +1,11 @@ +#import + +// 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; +}; diff --git a/Libraries/LibWeb/HTML/Worker.cpp b/Libraries/LibWeb/HTML/Worker.cpp index cb492078cbf..b81cbbdfbd7 100644 --- a/Libraries/LibWeb/HTML/Worker.cpp +++ b/Libraries/LibWeb/HTML/Worker.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include namespace Web::HTML { @@ -82,14 +83,14 @@ WebIDL::ExceptionOr> 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 port, WorkerOptions const& options) +void run_a_worker(Variant, GC::Ref> worker, URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr 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(url, options, port, outside_settings); + auto agent = outside_settings.realm().create(url, options, port, outside_settings); + worker.visit([&](auto worker) { worker->set_agent(agent); }); } // https://html.spec.whatwg.org/multipage/workers.html#dom-worker-terminate diff --git a/Libraries/LibWeb/HTML/Worker.h b/Libraries/LibWeb/HTML/Worker.h index 55b9964627c..1570b1d0d99 100644 --- a/Libraries/LibWeb/HTML/Worker.h +++ b/Libraries/LibWeb/HTML/Worker.h @@ -42,6 +42,8 @@ public: GC::Ptr 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 m_outside_port; GC::Ptr m_agent; - - void run_a_worker(URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr outside_port, WorkerOptions const& options); }; +void run_a_worker(Variant, GC::Ref> worker, URL::URL& url, EnvironmentSettingsObject& outside_settings, GC::Ptr outside_port, WorkerOptions const& options); + } diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 58567b80bae..aadb593fe66 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -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) diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index c89a3121fb3..54650c6120a 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -371,6 +371,7 @@ Set ShadowRealm ShadowRoot SharedArrayBuffer +SharedWorker SourceBuffer SourceBufferList StaticRange