ladybird/Libraries/LibWeb/HTML/SharedWorker.cpp
Timothy Flynn 70db474cf0 LibJS+LibWeb: Port interned bytecode strings to UTF-16
This was almost a no-op, except we intern JS exception messages. So the
bulk of this patch is porting exception messages to UTF-16.
2025-08-14 10:27:08 +02:00

184 lines
8.5 KiB
C++

/*
* 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"_utf16);
// 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);
}
}