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