diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn index 937dce5ba83bd7..7f27d50928f750 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/HTML/BUILD.gn @@ -31,6 +31,7 @@ source_set("HTML") { "ErrorEvent.cpp", "EventHandler.cpp", "EventNames.cpp", + "EventSource.cpp", "FileFilter.cpp", "Focus.cpp", "FormAssociatedElement.cpp", diff --git a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni index ba14085c28e76c..2a3785ef86c637 100644 --- a/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni +++ b/Meta/gn/secondary/Userland/Libraries/LibWeb/idl_files.gni @@ -119,6 +119,7 @@ standard_idl_files = [ "//Userland/Libraries/LibWeb/HTML/DOMParser.idl", "//Userland/Libraries/LibWeb/HTML/DOMStringMap.idl", "//Userland/Libraries/LibWeb/HTML/ErrorEvent.idl", + "//Userland/Libraries/LibWeb/HTML/EventSource.idl", "//Userland/Libraries/LibWeb/HTML/FormDataEvent.idl", "//Userland/Libraries/LibWeb/HTML/HashChangeEvent.idl", "//Userland/Libraries/LibWeb/HTML/History.idl", diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 9e8bbb2974af64..9a3f1aa3392c12 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -272,6 +272,7 @@ set(SOURCES HTML/DataTransfer.cpp HTML/ErrorEvent.cpp HTML/EventHandler.cpp + HTML/EventSource.cpp HTML/EventLoop/EventLoop.cpp HTML/EventLoop/Task.cpp HTML/EventLoop/TaskQueue.cpp diff --git a/Userland/Libraries/LibWeb/DOM/Document.cpp b/Userland/Libraries/LibWeb/DOM/Document.cpp index 802236bbeccf48..62cb2f4c0fb5d5 100644 --- a/Userland/Libraries/LibWeb/DOM/Document.cpp +++ b/Userland/Libraries/LibWeb/DOM/Document.cpp @@ -3118,7 +3118,8 @@ void Document::run_unloading_cleanup_steps() // 4. If document's salvageable state is false, then: if (!m_salvageable) { - // FIXME: 1. For each EventSource object eventSource whose relevant global object is equal to window, forcibly close eventSource. + // 1. For each EventSource object eventSource whose relevant global object is equal to window, forcibly close eventSource. + window->forcibly_close_all_event_sources(); // 2. Clear window's map of active timers. window->clear_map_of_active_timers(); diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 732e427816bb16..c0c92abed1582f 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -350,6 +350,7 @@ class DOMStringMap; class ErrorEvent; class EventHandler; class EventLoop; +class EventSource; class FormAssociatedElement; class FormDataEvent; class History; diff --git a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h index 756c856cd08f53..e16f4adf536349 100644 --- a/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h +++ b/Userland/Libraries/LibWeb/HTML/EventLoop/Task.h @@ -57,6 +57,9 @@ class Task final : public JS::Cell { // https://drafts.csswg.org/css-font-loading/#task-source FontLoading, + // https://html.spec.whatwg.org/multipage/server-sent-events.html#remote-event-task-source + RemoteEvent, + // !!! IMPORTANT: Keep this field last! // This serves as the base value of all unique task sources. // Some elements, such as the HTMLMediaElement, must have a unique task source per instance. diff --git a/Userland/Libraries/LibWeb/HTML/EventSource.cpp b/Userland/Libraries/LibWeb/HTML/EventSource.cpp new file mode 100644 index 00000000000000..b364e7031163fd --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/EventSource.cpp @@ -0,0 +1,457 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +JS_DEFINE_ALLOCATOR(EventSource); + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource +WebIDL::ExceptionOr> EventSource::construct_impl(JS::Realm& realm, StringView url, EventSourceInit event_source_init_dict) +{ + auto& vm = realm.vm(); + + // 1. Let ev be a new EventSource object. + auto event_source = realm.heap().allocate(realm, realm); + + // 2. Let settings be ev's relevant settings object. + auto& settings = relevant_settings_object(event_source); + + // 3. Let urlRecord be the result of encoding-parsing a URL given url, relative to settings. + auto url_record = settings.parse_url(url); + + // 4. If urlRecord is failure, then throw a "SyntaxError" DOMException. + if (!url_record.is_valid()) + return WebIDL::SyntaxError::create(realm, MUST(String::formatted("Invalid URL '{}'", url))); + + // 5. Set ev's url to urlRecord. + event_source->m_url = move(url_record); + + // 6. Let corsAttributeState be Anonymous. + auto cors_attribute_state = CORSSettingAttribute::Anonymous; + + // 7. If the value of eventSourceInitDict's withCredentials member is true, then set corsAttributeState to Use Credentials + // and set ev's withCredentials attribute to true. + if (event_source_init_dict.with_credentials) { + cors_attribute_state = CORSSettingAttribute::UseCredentials; + event_source->m_with_credentials = true; + } + + // 8. Let request be the result of creating a potential-CORS request given urlRecord, the empty string, and corsAttributeState. + auto request = create_potential_CORS_request(vm, event_source->m_url, {}, cors_attribute_state); + + // 9. Set request's client to settings. + request->set_client(&settings); + + // 10. User agents may set (`Accept`, `text/event-stream`) in request's header list. + auto header = Fetch::Infrastructure::Header::from_string_pair("Accept"sv, "text/event-stream"sv); + request->header_list()->set(move(header)); + + // 11. Set request's cache mode to "no-store". + request->set_cache_mode(Fetch::Infrastructure::Request::CacheMode::NoStore); + + // 12. Set request's initiator type to "other". + request->set_initiator_type(Fetch::Infrastructure::Request::InitiatorType::Other); + + // AD-HOC: We must not buffer the response as the connection generally never ends, thus we can't wait for the end + // of the response body. + request->set_buffer_policy(Fetch::Infrastructure::Request::BufferPolicy::DoNotBufferResponse); + + // 13. Set ev's request to request. + event_source->m_request = request; + + // 14. Let processEventSourceEndOfBody given response res be the following step: if res is not a network error, then + // reestablish the connection. + auto process_event_source_end_of_body = [event_source](JS::NonnullGCPtr response) { + if (!response->is_network_error()) + event_source->reestablish_the_connection(); + }; + + // 15. Fetch request, with processResponseEndOfBody set to processEventSourceEndOfBody and processResponse set to the + // following steps given response res: + Fetch::Infrastructure::FetchAlgorithms::Input fetch_algorithms_input {}; + fetch_algorithms_input.process_response_end_of_body = move(process_event_source_end_of_body); + + fetch_algorithms_input.process_response = [event_source](JS::NonnullGCPtr response) { + auto& realm = event_source->realm(); + + // FIXME: If the response is CORS cross-origin, we must use its internal response to query any of its data. See: + // https://github.com/whatwg/html/issues/9355 + response = response->unsafe_response(); + + auto content_type_is_text_event_stream = [&]() { + auto content_type = response->header_list()->extract_mime_type(); + if (!content_type.has_value()) + return false; + + return content_type->essence() == "text/event-stream"sv; + }; + + // 1. If res is an aborted network error, then fail the connection. + if (response->is_aborted_network_error()) { + event_source->fail_the_connection(); + } + // 2. Otherwise, if res is a network error, then reestablish the connection, unless the user agent knows that + // to be futile, in which case the user agent may fail the connection. + else if (response->is_network_error()) { + event_source->reestablish_the_connection(); + } + // 3. Otherwise, if res's status is not 200, or if res's `Content-Type` is not `text/event-stream`, then fail + // the connection. + else if (response->status() != 200 || !content_type_is_text_event_stream()) { + event_source->fail_the_connection(); + } + // 4. Otherwise, announce the connection and interpret res's body line by line. + else { + event_source->announce_the_connection(); + + auto process_body_chunk = JS::create_heap_function(realm.heap(), [event_source](ByteBuffer body) { + event_source->interpret_response(body); + }); + auto process_end_of_body = JS::create_heap_function(realm.heap(), []() { + // This case is handled by `process_event_source_end_of_body` above. + }); + auto process_body_error = JS::create_heap_function(realm.heap(), [](JS::Value) { + // This case is handled by `process_event_source_end_of_body` above. + }); + + response->body()->incrementally_read(process_body_chunk, process_end_of_body, process_body_error, { realm.global_object() }); + } + }; + + event_source->m_fetch_algorithms = Fetch::Infrastructure::FetchAlgorithms::create(vm, move(fetch_algorithms_input)); + event_source->m_fetch_controller = TRY(Fetch::Fetching::fetch(realm, request, *event_source->m_fetch_algorithms)); + + // 16. Return ev. + return event_source; +} + +EventSource::EventSource(JS::Realm& realm) + : EventTarget(realm) +{ +} + +EventSource::~EventSource() = default; + +void EventSource::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(EventSource); + + auto* relevant_global = dynamic_cast(&HTML::relevant_global_object(*this)); + VERIFY(relevant_global); + relevant_global->register_event_source({}, *this); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#garbage-collection +void EventSource::finalize() +{ + // If an EventSource object is garbage collected while its connection is still open, the user agent must abort any + // instance of the fetch algorithm opened by this EventSource. + if (m_ready_state != ReadyState::Closed) { + if (m_fetch_controller) + m_fetch_controller->abort(realm(), {}); + } + + auto* relevant_global = dynamic_cast(&HTML::relevant_global_object(*this)); + VERIFY(relevant_global); + relevant_global->unregister_event_source({}, *this); +} + +void EventSource::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_request); + visitor.visit(m_fetch_algorithms); + visitor.visit(m_fetch_controller); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onopen +void EventSource::set_onopen(WebIDL::CallbackType* event_handler) +{ + set_event_handler_attribute(HTML::EventNames::open, event_handler); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onopen +WebIDL::CallbackType* EventSource::onopen() +{ + return event_handler_attribute(HTML::EventNames::open); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onmessage +void EventSource::set_onmessage(WebIDL::CallbackType* event_handler) +{ + set_event_handler_attribute(HTML::EventNames::message, event_handler); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onmessage +WebIDL::CallbackType* EventSource::onmessage() +{ + return event_handler_attribute(HTML::EventNames::message); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onerror +void EventSource::set_onerror(WebIDL::CallbackType* event_handler) +{ + set_event_handler_attribute(HTML::EventNames::error, event_handler); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#handler-eventsource-onerror +WebIDL::CallbackType* EventSource::onerror() +{ + return event_handler_attribute(HTML::EventNames::error); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-close +void EventSource::close() +{ + // The close() method must abort any instances of the fetch algorithm started for this EventSource object, and must + // set the readyState attribute to CLOSED. + if (m_fetch_controller) + m_fetch_controller->abort(realm(), {}); + + m_ready_state = ReadyState::Closed; +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-eventsource-forcibly-close +void EventSource::forcibly_close() +{ + // If a user agent is to forcibly close an EventSource object (this happens when a Document object goes away + // permanently), the user agent must abort any instances of the fetch algorithm started for this EventSource + // object, and must set the readyState attribute to CLOSED. + if (m_fetch_controller) + m_fetch_controller->abort(realm(), {}); + + m_ready_state = ReadyState::Closed; +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#announce-the-connection +void EventSource::announce_the_connection() +{ + // When a user agent is to announce the connection, the user agent must queue a task which, if the readyState attribute + // is set to a value other than CLOSED, sets the readyState attribute to OPEN and fires an event named open at the + // EventSource object. + HTML::queue_a_task(HTML::Task::Source::RemoteEvent, nullptr, nullptr, JS::create_heap_function(heap(), [this]() { + if (m_ready_state != ReadyState::Closed) { + m_ready_state = ReadyState::Open; + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::open)); + } + })); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#reestablish-the-connection +void EventSource::reestablish_the_connection() +{ + bool initial_task_has_run { false }; + + // 1. Queue a task to run the following steps: + HTML::queue_a_task(HTML::Task::Source::RemoteEvent, nullptr, nullptr, JS::create_heap_function(heap(), [&]() { + ScopeGuard guard { [&]() { initial_task_has_run = true; } }; + + // 1. If the readyState attribute is set to CLOSED, abort the task. + if (m_ready_state == ReadyState::Closed) + return; + + // 2. Set the readyState attribute to CONNECTING. + m_ready_state = ReadyState::Connecting; + + // 3. Fire an event named error at the EventSource object. + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error)); + })); + + // 2. Wait a delay equal to the reconnection time of the event source. + HTML::main_thread_event_loop().spin_until([&, delay_start = MonotonicTime::now()]() { + return (MonotonicTime::now() - delay_start) >= m_reconnection_time; + }); + + // 3. Optionally, wait some more. In particular, if the previous attempt failed, then user agents might introduce + // an exponential backoff delay to avoid overloading a potentially already overloaded server. Alternatively, if + // the operating system has reported that there is no network connectivity, user agents might wait for the + // operating system to announce that the network connection has returned before retrying. + + // 4. Wait until the aforementioned task has run, if it has not yet run. + if (!initial_task_has_run) { + HTML::main_thread_event_loop().spin_until([&]() { return initial_task_has_run; }); + } + + // 5. Queue a task to run the following steps: + HTML::queue_a_task(HTML::Task::Source::RemoteEvent, nullptr, nullptr, JS::create_heap_function(heap(), [this]() { + // 1. If the EventSource object's readyState attribute is not set to CONNECTING, then return. + if (m_ready_state != ReadyState::Connecting) + return; + + // 2. Let request be the EventSource object's request. + JS::NonnullGCPtr request { *m_request }; + + // 3. If the EventSource object's last event ID string is not the empty string, then: + if (!m_last_event_id.is_empty()) { + // 1. Let lastEventIDValue be the EventSource object's last event ID string, encoded as UTF-8. + // 2. Set (`Last-Event-ID`, lastEventIDValue) in request's header list. + auto header = Fetch::Infrastructure::Header::from_string_pair("Last-Event-ID"sv, m_last_event_id); + request->header_list()->set(header); + } + + // 4. Fetch request and process the response obtained in this fashion, if any, as described earlier in this section. + m_fetch_controller = Fetch::Fetching::fetch(realm(), request, *m_fetch_algorithms).release_value_but_fixme_should_propagate_errors(); + })); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#fail-the-connection +void EventSource::fail_the_connection() +{ + // When a user agent is to fail the connection, the user agent must queue a task which, if the readyState attribute + // is set to a value other than CLOSED, sets the readyState attribute to CLOSED and fires an event named error at the + // EventSource object. Once the user agent has failed the connection, it does not attempt to reconnect. + HTML::queue_a_task(HTML::Task::Source::RemoteEvent, nullptr, nullptr, JS::create_heap_function(heap(), [this]() { + if (m_ready_state != ReadyState::Closed) { + m_ready_state = ReadyState::Closed; + dispatch_event(DOM::Event::create(realm(), HTML::EventNames::error)); + } + })); +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation +void EventSource::interpret_response(StringView response) +{ + // Lines must be processed, in the order they are received, as follows: + for (auto line : response.lines(StringView::ConsiderCarriageReturn::Yes)) { + // -> If the line is empty (a blank line) + if (line.is_empty()) { + // Dispatch the event, as defined below. + dispatch_the_event(); + } + // -> If the line starts with a U+003A COLON character (:) + else if (line.starts_with(':')) { + // Ignore the line. + } + // -> If the line contains a U+003A COLON character (:) + else if (auto index = line.find(':'); index.has_value()) { + // Collect the characters on the line before the first U+003A COLON character (:), and let field be that string. + auto field = line.substring_view(0, *index); + + // Collect the characters on the line after the first U+003A COLON character (:), and let value be that string. + // If value starts with a U+0020 SPACE character, remove it from value. + auto value = line.substring_view(*index + 1); + + if (value.starts_with(' ')) + value = value.substring_view(1); + + // Process the field using the steps described below, using field as the field name and value as the field value. + process_field(field, value); + } + // -> Otherwise, the string is not empty but does not contain a U+003A COLON character (:) + else { + // Process the field using the steps described below, using the whole line as the field name, and the empty + // string as the field value. + process_field(line, {}); + } + } +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#processField +void EventSource::process_field(StringView field, StringView value) +{ + // -> If the field name is "event" + if (field == "event"sv) { + // Set the event type buffer to field value. + m_event_type = MUST(String::from_utf8(value)); + } + // -> If the field name is "data" + else if (field == "data"sv) { + // Append the field value to the data buffer, then append a single U+000A LINE FEED (LF) character to the data buffer. + m_data.append(value); + m_data.append('\n'); + } + // -> If the field name is "id" + else if (field == "id"sv) { + // If the field value does not contain U+0000 NULL, then set the last event ID buffer to the field value. + // Otherwise, ignore the field. + if (!value.contains('\0')) + m_last_event_id = MUST(String::from_utf8(value)); + } + // -> If the field name is "retry" + else if (field == "retry"sv) { + // If the field value consists of only ASCII digits, then interpret the field value as an integer in base ten, + // and set the event stream's reconnection time to that integer. Otherwise, ignore the field. + if (auto retry = value.to_number(); retry.has_value()) + m_reconnection_time = Duration::from_seconds(*retry); + } + // -> Otherwise + else { + // The field is ignored. + } +} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#dispatchMessage +void EventSource::dispatch_the_event() +{ + // 1. Set the last event ID string of the event source to the value of the last event ID buffer. The buffer does not + // get reset, so the last event ID string of the event source remains set to this value until the next time it is + // set by the server. + auto const& last_event_id = m_last_event_id; + + // 2. If the data buffer is an empty string, set the data buffer and the event type buffer to the empty string and return. + auto data_buffer = m_data.string_view(); + + if (data_buffer.is_empty()) { + m_event_type = {}; + m_data.clear(); + return; + } + + // 3. If the data buffer's last character is a U+000A LINE FEED (LF) character, then remove the last character from the data buffer. + if (data_buffer.ends_with('\n')) + data_buffer = data_buffer.substring_view(0, data_buffer.length() - 1); + + // 4. Let event be the result of creating an event using MessageEvent, in the relevant realm of the EventSource object. + // 5. Initialize event's type attribute to "message", its data attribute to data, its origin attribute to the serialization + // of the origin of the event stream's final URL (i.e., the URL after redirects), and its lastEventId attribute to the + // last event ID string of the event source. + // 6. If the event type buffer has a value other than the empty string, change the type of the newly created event to equal + // the value of the event type buffer. + MessageEventInit init {}; + init.data = JS::PrimitiveString::create(vm(), data_buffer); + init.origin = MUST(String::from_byte_string(m_url.serialize_origin())); + init.last_event_id = last_event_id; + + auto type = m_event_type.is_empty() ? HTML::EventNames::message : m_event_type; + auto event = MessageEvent::create(realm(), type, init); + + // 7. Set the data buffer and the event type buffer to the empty string. + m_event_type = {}; + m_data.clear(); + + // 8. Queue a task which, if the readyState attribute is set to a value other than CLOSED, dispatches the newly created + // event at the EventSource object. + HTML::queue_a_task(HTML::Task::Source::RemoteEvent, nullptr, nullptr, JS::create_heap_function(heap(), [this, event]() { + if (m_ready_state != ReadyState::Closed) + dispatch_event(event); + })); +} + +} diff --git a/Userland/Libraries/LibWeb/HTML/EventSource.h b/Userland/Libraries/LibWeb/HTML/EventSource.h new file mode 100644 index 00000000000000..26998dd0eafcb0 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/EventSource.h @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2024, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::HTML { + +struct EventSourceInit { + bool with_credentials { false }; +}; + +class EventSource : public DOM::EventTarget { + WEB_PLATFORM_OBJECT(EventSource, DOM::EventTarget); + JS_DECLARE_ALLOCATOR(EventSource); + +public: + virtual ~EventSource() override; + + static WebIDL::ExceptionOr> construct_impl(JS::Realm&, StringView url, EventSourceInit event_source_init_dict = {}); + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-url + String url() const { return MUST(String::from_byte_string(m_url.serialize())); } + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-withcredentials + bool with_credentials() const { return m_with_credentials; } + + enum class ReadyState : WebIDL::UnsignedShort { + Connecting = 0, + Open = 1, + Closed = 2, + }; + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate + ReadyState ready_state() const { return m_ready_state; } + + void set_onopen(WebIDL::CallbackType*); + WebIDL::CallbackType* onopen(); + + void set_onmessage(WebIDL::CallbackType*); + WebIDL::CallbackType* onmessage(); + + void set_onerror(WebIDL::CallbackType*); + WebIDL::CallbackType* onerror(); + + void close(); + void forcibly_close(); + +private: + explicit EventSource(JS::Realm&); + + virtual void initialize(JS::Realm&) override; + virtual void finalize() override; + virtual void visit_edges(Cell::Visitor&) override; + + void announce_the_connection(); + void reestablish_the_connection(); + void fail_the_connection(); + + void interpret_response(StringView); + void process_field(StringView field, StringView value); + void dispatch_the_event(); + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-eventsource-url + URL::URL m_url; + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-event-stream-request + JS::GCPtr m_request; + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-event-stream-reconnection-time + Duration m_reconnection_time { Duration::from_seconds(3) }; + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#concept-event-stream-last-event-id + String m_last_event_id; + + String m_event_type; + StringBuilder m_data; + + bool m_with_credentials { false }; + + ReadyState m_ready_state { ReadyState::Connecting }; + + JS::GCPtr m_fetch_algorithms; + JS::GCPtr m_fetch_controller; +}; + +} diff --git a/Userland/Libraries/LibWeb/HTML/EventSource.idl b/Userland/Libraries/LibWeb/HTML/EventSource.idl new file mode 100644 index 00000000000000..5790a1faa65e86 --- /dev/null +++ b/Userland/Libraries/LibWeb/HTML/EventSource.idl @@ -0,0 +1,27 @@ +#import +#import + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#eventsource +[Exposed=(Window,Worker)] +interface EventSource : EventTarget { + constructor(USVString url, optional EventSourceInit eventSourceInitDict = {}); + + readonly attribute USVString url; + readonly attribute boolean withCredentials; + + // ready state + const unsigned short CONNECTING = 0; + const unsigned short OPEN = 1; + const unsigned short CLOSED = 2; + readonly attribute unsigned short readyState; + + // networking + attribute EventHandler onopen; + attribute EventHandler onmessage; + attribute EventHandler onerror; + undefined close(); +}; + +dictionary EventSourceInit { + boolean withCredentials = false; +}; diff --git a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp index afad8ed1b4cede..486ee97987110a 100644 --- a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp +++ b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -70,6 +71,7 @@ void WindowOrWorkerGlobalScopeMixin::visit_edges(JS::Cell::Visitor& visitor) visitor.visit(m_indexed_db); for (auto& entry : m_performance_entry_buffer_map) entry.value.visit_edges(visitor); + visitor.visit(m_registered_event_sources); } void WindowOrWorkerGlobalScopeMixin::finalize() @@ -649,6 +651,22 @@ void WindowOrWorkerGlobalScopeMixin::queue_the_performance_observer_task() })); } +void WindowOrWorkerGlobalScopeMixin::register_event_source(Badge, JS::NonnullGCPtr event_source) +{ + m_registered_event_sources.set(event_source); +} + +void WindowOrWorkerGlobalScopeMixin::unregister_event_source(Badge, JS::NonnullGCPtr event_source) +{ + m_registered_event_sources.remove(event_source); +} + +void WindowOrWorkerGlobalScopeMixin::forcibly_close_all_event_sources() +{ + for (auto event_source : m_registered_event_sources) + event_source->forcibly_close(); +} + // https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#run-steps-after-a-timeout void WindowOrWorkerGlobalScopeMixin::run_steps_after_a_timeout(i32 timeout, Function completion_step) { diff --git a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h index 7b05710cb411b1..5dbdd307e33930 100644 --- a/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h +++ b/Userland/Libraries/LibWeb/HTML/WindowOrWorkerGlobalScope.h @@ -64,6 +64,10 @@ class WindowOrWorkerGlobalScopeMixin { void queue_the_performance_observer_task(); + void register_event_source(Badge, JS::NonnullGCPtr); + void unregister_event_source(Badge, JS::NonnullGCPtr); + void forcibly_close_all_event_sources(); + void run_steps_after_a_timeout(i32 timeout, Function completion_step); [[nodiscard]] JS::NonnullGCPtr performance(); @@ -103,6 +107,8 @@ class WindowOrWorkerGlobalScopeMixin { // NOTE: See the PerformanceEntryTuple struct above for the map's value tuple. OrderedHashMap m_performance_entry_buffer_map; + HashTable> m_registered_event_sources; + JS::GCPtr m_performance; JS::GCPtr m_indexed_db; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 8e7eef09432fab..c68f02d7fdf217 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -103,6 +103,7 @@ libweb_js_bindings(HTML/DOMParser) libweb_js_bindings(HTML/DOMStringMap) libweb_js_bindings(HTML/DataTransfer) libweb_js_bindings(HTML/ErrorEvent) +libweb_js_bindings(HTML/EventSource) libweb_js_bindings(HTML/FormDataEvent) libweb_js_bindings(HTML/HashChangeEvent) libweb_js_bindings(HTML/History)