From e93560b769f5749e96ca66977886e2be94554550 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sun, 2 Apr 2023 15:15:53 -0700 Subject: [PATCH] LibWeb: Add the WritableStream interface --- Userland/Libraries/LibWeb/CMakeLists.txt | 2 + Userland/Libraries/LibWeb/Forward.h | 2 + .../LibWeb/Streams/AbstractOperations.cpp | 13 ++ .../LibWeb/Streams/AbstractOperations.h | 2 + .../LibWeb/Streams/UnderlyingSink.cpp | 35 +++++ .../Libraries/LibWeb/Streams/UnderlyingSink.h | 26 ++++ .../LibWeb/Streams/WritableStream.cpp | 86 ++++++++++++ .../Libraries/LibWeb/Streams/WritableStream.h | 132 ++++++++++++++++++ .../LibWeb/Streams/WritableStream.idl | 12 ++ Userland/Libraries/LibWeb/idl_files.cmake | 1 + 10 files changed, 311 insertions(+) create mode 100644 Userland/Libraries/LibWeb/Streams/UnderlyingSink.cpp create mode 100644 Userland/Libraries/LibWeb/Streams/UnderlyingSink.h create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStream.cpp create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStream.h create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStream.idl diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 3785dc2cb0..f9d315127f 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -462,7 +462,9 @@ set(SOURCES Streams/ReadableStreamDefaultController.cpp Streams/ReadableStreamDefaultReader.cpp Streams/ReadableStreamGenericReader.cpp + Streams/UnderlyingSink.cpp Streams/UnderlyingSource.cpp + Streams/WritableStream.cpp SVG/AttributeNames.cpp SVG/AttributeParser.cpp SVG/SVGAnimatedLength.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index b20e081070..a1fc6b9f60 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -426,7 +426,9 @@ class ReadableStreamDefaultController; class ReadableStreamDefaultReader; class ReadableStreamGenericReaderMixin; class ReadRequest; +struct UnderlyingSink; struct UnderlyingSource; +class WritableStream; } namespace Web::SVG { diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp index 0ed20ddebb..367883635a 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -13,7 +13,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -734,6 +736,17 @@ WebIDL::ExceptionOr set_up_readable_stream_default_controller_from_underly return set_up_readable_stream_default_controller(stream, controller, move(start_algorithm), move(pull_algorithm), move(cancel_algorithm), high_water_mark, move(size_algorithm)); } +// https://streams.spec.whatwg.org/#is-writable-stream-locked +bool is_writable_stream_locked(WritableStream const& stream) +{ + // 1. If stream.[[writer]] is undefined, return false. + if (!stream.writer()) + return false; + + // 2. Return true. + return true; +} + // Non-standard function to aid in converting a user-provided function into a WebIDL::Callback. This is essentially // what the Bindings generator would do at compile time, but at runtime instead. JS::ThrowCompletionOr> property_to_callback(JS::VM& vm, JS::Value value, JS::PropertyKey const& property_key) diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h index 15a47f6ad4..2ca1356610 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h @@ -50,6 +50,8 @@ bool readable_stream_default_controller_can_close_or_enqueue(ReadableStreamDefau WebIDL::ExceptionOr set_up_readable_stream_default_controller(ReadableStream&, ReadableStreamDefaultController&, StartAlgorithm&&, PullAlgorithm&&, CancelAlgorithm&&, double high_water_mark, SizeAlgorithm&&); WebIDL::ExceptionOr set_up_readable_stream_default_controller_from_underlying_source(ReadableStream&, JS::Value underlying_source_value, UnderlyingSource, double high_water_mark, SizeAlgorithm&&); +bool is_writable_stream_locked(WritableStream const&); + JS::ThrowCompletionOr> property_to_callback(JS::VM& vm, JS::Value value, JS::PropertyKey const& property_key); } diff --git a/Userland/Libraries/LibWeb/Streams/UnderlyingSink.cpp b/Userland/Libraries/LibWeb/Streams/UnderlyingSink.cpp new file mode 100644 index 0000000000..ab12c8c55b --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/UnderlyingSink.cpp @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include + +namespace Web::Streams { + +JS::ThrowCompletionOr UnderlyingSink::from_value(JS::VM& vm, JS::Value value) +{ + if (!value.is_object()) + return UnderlyingSink {}; + + auto& object = value.as_object(); + + UnderlyingSink underlying_sink { + .start = TRY(property_to_callback(vm, value, "start")), + .write = TRY(property_to_callback(vm, value, "write")), + .close = TRY(property_to_callback(vm, value, "close")), + .abort = TRY(property_to_callback(vm, value, "abort")), + .type = {}, + }; + + if (TRY(object.has_property("type"))) + underlying_sink.type = TRY(object.get("type")); + + return underlying_sink; +} + +} diff --git a/Userland/Libraries/LibWeb/Streams/UnderlyingSink.h b/Userland/Libraries/LibWeb/Streams/UnderlyingSink.h new file mode 100644 index 0000000000..766ebcd1b6 --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/UnderlyingSink.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#dictdef-underlyingsink +struct UnderlyingSink { + JS::Handle start; + JS::Handle write; + JS::Handle close; + JS::Handle abort; + Optional type; + + static JS::ThrowCompletionOr from_value(JS::VM&, JS::Value); +}; + +} diff --git a/Userland/Libraries/LibWeb/Streams/WritableStream.cpp b/Userland/Libraries/LibWeb/Streams/WritableStream.cpp new file mode 100644 index 0000000000..b855f13e6f --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStream.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#ws-constructor +WebIDL::ExceptionOr> WritableStream::construct_impl(JS::Realm& realm, Optional> const& underlying_sink_object) +{ + auto& vm = realm.vm(); + + auto writable_stream = MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm)); + + // 1. If underlyingSink is missing, set it to null. + auto underlying_sink = underlying_sink_object.has_value() ? JS::Value(underlying_sink_object.value().ptr()) : JS::js_null(); + + // 2. Let underlyingSinkDict be underlyingSink, converted to an IDL value of type UnderlyingSink. + auto underlying_sink_dict = TRY(UnderlyingSink::from_value(vm, underlying_sink)); + + // 3. If underlyingSinkDict["type"] exists, throw a RangeError exception. + if (!underlying_sink_dict.type.has_value()) + return WebIDL::SimpleException { WebIDL::SimpleExceptionType::RangeError, "Invalid use of reserved key 'type'"sv }; + + // 4. Perform ! InitializeWritableStream(this). + // Note: This AO configures slot values which are already specified in the class's field initializers. + + // FIXME: 5. Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). + SizeAlgorithm size_algorithm = [](auto const&) { return JS::normal_completion(JS::Value(1)); }; + + // FIXME: 6. Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). + auto high_water_mark = 1.0; + + // FIXME: 7. Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm). + (void)high_water_mark; + + return writable_stream; +} + +// https://streams.spec.whatwg.org/#ws-locked +bool WritableStream::locked() const +{ + // 1. Return ! IsWritableStreamLocked(this). + return is_writable_stream_locked(*this); +} + +WritableStream::WritableStream(JS::Realm& realm) + : Bindings::PlatformObject(realm) +{ +} + +JS::ThrowCompletionOr WritableStream::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype(realm, "WritableStream")); + + return {}; +} + +void WritableStream::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_close_request); + visitor.visit(m_controller); + visitor.visit(m_in_flight_write_request); + visitor.visit(m_in_flight_close_request); + if (m_pending_abort_request.has_value()) { + visitor.visit(m_pending_abort_request->promise); + visitor.visit(m_pending_abort_request->reason); + } + visitor.visit(m_stored_error); + visitor.visit(m_writer); + for (auto& write_request : m_write_requests) + visitor.visit(write_request); +} + +} diff --git a/Userland/Libraries/LibWeb/Streams/WritableStream.h b/Userland/Libraries/LibWeb/Streams/WritableStream.h new file mode 100644 index 0000000000..e0a471eb24 --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStream.h @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#pending-abort-request +struct PendingAbortRequest { + // https://streams.spec.whatwg.org/#pending-abort-request-promise + // A promise returned from WritableStreamAbort + JS::NonnullGCPtr promise; + + // https://streams.spec.whatwg.org/#pending-abort-request-reason + // A JavaScript value that was passed as the abort reason to WritableStreamAbort + JS::Value reason; + + // https://streams.spec.whatwg.org/#pending-abort-request-was-already-erroring + // A boolean indicating whether or not the stream was in the "erroring" state when WritableStreamAbort was called, which impacts the outcome of the abort request + bool was_already_erroring; +}; + +// https://streams.spec.whatwg.org/#writablestream +class WritableStream final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(WritableStream, Bindings::PlatformObject); + +public: + enum class State { + Writable, + Closed, + Erroring, + Errored, + }; + + static WebIDL::ExceptionOr> construct_impl(JS::Realm& realm, Optional> const& underlying_sink); + + virtual ~WritableStream() = default; + + bool locked() const; + + bool backpressure() const { return m_backpressure; } + void set_backpressure(bool value) { m_backpressure = value; } + + JS::GCPtr close_request() const { return m_close_request; } + void set_close_request(JS::GCPtr value) { m_close_request = value; } + + JS::GCPtr controller() { return m_controller; } + void set_controller(JS::GCPtr value) { m_controller = value; } + + JS::GCPtr in_flight_write_request() const { return m_in_flight_write_request; } + void set_in_flight_write_request(JS::GCPtr value) { m_in_flight_write_request = value; } + + JS::GCPtr in_flight_close_request() const { return m_in_flight_close_request; } + void set_in_flight_close_request(JS::GCPtr value) { m_in_flight_close_request = value; } + + Optional& pending_abort_request() { return m_pending_abort_request; } + void set_pending_abort_request(Optional&& value) { m_pending_abort_request = move(value); } + + State state() const { return m_state; } + void set_state(State value) { m_state = value; } + + JS::Value stored_error() const { return m_stored_error; } + void set_stored_error(JS::Value value) { m_stored_error = value; } + + JS::GCPtr writer() const { return m_writer; } + void set_writer(JS::GCPtr value) { m_writer = value; } + + SinglyLinkedList>& write_requests() { return m_write_requests; } + +private: + explicit WritableStream(JS::Realm&); + + virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + + virtual void visit_edges(Cell::Visitor&) override; + + // https://streams.spec.whatwg.org/#writablestream-backpressure + // A boolean indicating the backpressure signal set by the controller + bool m_backpressure { false }; + + // https://streams.spec.whatwg.org/#writablestream-closerequest + // The promise returned from the writer’s close() method + JS::GCPtr m_close_request; + + // https://streams.spec.whatwg.org/#writablestream-controller + // A WritableStreamDefaultController created with the ability to control the state and queue of this stream + JS::GCPtr m_controller; + + // https://streams.spec.whatwg.org/#writablestream-detached + // A boolean flag set to true when the stream is transferred + bool m_detached { false }; + + // https://streams.spec.whatwg.org/#writablestream-inflightwriterequest + // A slot set to the promise for the current in-flight write operation while the underlying sink's write algorithm is executing and has not yet fulfilled, used to prevent reentrant calls + JS::GCPtr m_in_flight_write_request; + + // https://streams.spec.whatwg.org/#writablestream-inflightcloserequest + // A slot set to the promise for the current in-flight close operation while the underlying sink's close algorithm is executing and has not yet fulfilled, used to prevent the abort() method from interrupting close + JS::GCPtr m_in_flight_close_request; + + // https://streams.spec.whatwg.org/#writablestream-pendingabortrequest + // A pending abort request + Optional m_pending_abort_request; + + // https://streams.spec.whatwg.org/#writablestream-state + // A string containing the stream’s current state, used internally; one of "writable", "closed", "erroring", or "errored" + State m_state { State::Writable }; + + // https://streams.spec.whatwg.org/#writablestream-storederror + // A value indicating how the stream failed, to be given as a failure reason or exception when trying to operate on the stream while in the "errored" state + JS::Value m_stored_error { JS::js_undefined() }; + + // https://streams.spec.whatwg.org/#writablestream-writer + // A WritableStreamDefaultWriter instance, if the stream is locked to a writer, or undefined if it is not + JS::GCPtr m_writer; + + // https://streams.spec.whatwg.org/#writablestream-writerequests + // A list of promises representing the stream’s internal queue of write requests not yet processed by the underlying sink + SinglyLinkedList> m_write_requests; +}; + +} diff --git a/Userland/Libraries/LibWeb/Streams/WritableStream.idl b/Userland/Libraries/LibWeb/Streams/WritableStream.idl new file mode 100644 index 0000000000..039cccb43f --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStream.idl @@ -0,0 +1,12 @@ +[Exposed=*, Transferable] +interface WritableStream { + // FIXME: optional QueuingStrategy strategy = {} + constructor(optional object underlyingSink); + + readonly attribute boolean locked; + + // FIXME: + // Promise abort(optional any reason); + // Promise close(); + // WritableStreamDefaultWriter getWriter(); +}; diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index 8c162a1911..79e9020d2f 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -179,6 +179,7 @@ libweb_js_bindings(ResizeObserver/ResizeObserver) libweb_js_bindings(Streams/ReadableStream) libweb_js_bindings(Streams/ReadableStreamDefaultController) libweb_js_bindings(Streams/ReadableStreamDefaultReader) +libweb_js_bindings(Streams/WritableStream) libweb_js_bindings(SVG/SVGAnimatedLength) libweb_js_bindings(SVG/SVGClipPathElement) libweb_js_bindings(SVG/SVGDefsElement)