Procházet zdrojové kódy

LibWeb: Add the WritableStream interface

Matthew Olsson před 2 roky
rodič
revize
e93560b769

+ 2 - 0
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

+ 2 - 0
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 {

+ 13 - 0
Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp

@@ -13,7 +13,9 @@
 #include <LibWeb/Streams/ReadableStreamDefaultController.h>
 #include <LibWeb/Streams/ReadableStreamDefaultReader.h>
 #include <LibWeb/Streams/ReadableStreamGenericReader.h>
+#include <LibWeb/Streams/UnderlyingSink.h>
 #include <LibWeb/Streams/UnderlyingSource.h>
+#include <LibWeb/Streams/WritableStream.h>
 #include <LibWeb/WebIDL/AbstractOperations.h>
 #include <LibWeb/WebIDL/ExceptionOr.h>
 #include <LibWeb/WebIDL/Promise.h>
@@ -734,6 +736,17 @@ WebIDL::ExceptionOr<void> 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<JS::Handle<WebIDL::CallbackType>> property_to_callback(JS::VM& vm, JS::Value value, JS::PropertyKey const& property_key)

+ 2 - 0
Userland/Libraries/LibWeb/Streams/AbstractOperations.h

@@ -50,6 +50,8 @@ bool readable_stream_default_controller_can_close_or_enqueue(ReadableStreamDefau
 WebIDL::ExceptionOr<void> set_up_readable_stream_default_controller(ReadableStream&, ReadableStreamDefaultController&, StartAlgorithm&&, PullAlgorithm&&, CancelAlgorithm&&, double high_water_mark, SizeAlgorithm&&);
 WebIDL::ExceptionOr<void> 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<JS::Handle<WebIDL::CallbackType>> property_to_callback(JS::VM& vm, JS::Value value, JS::PropertyKey const& property_key);
 
 }

+ 35 - 0
Userland/Libraries/LibWeb/Streams/UnderlyingSink.cpp

@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/VM.h>
+#include <LibWeb/Streams/AbstractOperations.h>
+#include <LibWeb/Streams/UnderlyingSink.h>
+#include <LibWeb/WebIDL/CallbackType.h>
+
+namespace Web::Streams {
+
+JS::ThrowCompletionOr<UnderlyingSink> 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;
+}
+
+}

+ 26 - 0
Userland/Libraries/LibWeb/Streams/UnderlyingSink.h

@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Forward.h>
+#include <LibJS/Forward.h>
+#include <LibWeb/Forward.h>
+
+namespace Web::Streams {
+
+// https://streams.spec.whatwg.org/#dictdef-underlyingsink
+struct UnderlyingSink {
+    JS::Handle<WebIDL::CallbackType> start;
+    JS::Handle<WebIDL::CallbackType> write;
+    JS::Handle<WebIDL::CallbackType> close;
+    JS::Handle<WebIDL::CallbackType> abort;
+    Optional<JS::Value> type;
+
+    static JS::ThrowCompletionOr<UnderlyingSink> from_value(JS::VM&, JS::Value);
+};
+
+}

+ 86 - 0
Userland/Libraries/LibWeb/Streams/WritableStream.cpp

@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <LibJS/Runtime/PromiseCapability.h>
+#include <LibWeb/Bindings/Intrinsics.h>
+#include <LibWeb/Bindings/WritableStreamPrototype.h>
+#include <LibWeb/Streams/AbstractOperations.h>
+#include <LibWeb/Streams/UnderlyingSink.h>
+#include <LibWeb/Streams/WritableStream.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
+
+namespace Web::Streams {
+
+// https://streams.spec.whatwg.org/#ws-constructor
+WebIDL::ExceptionOr<JS::NonnullGCPtr<WritableStream>> WritableStream::construct_impl(JS::Realm& realm, Optional<JS::Handle<JS::Object>> const& underlying_sink_object)
+{
+    auto& vm = realm.vm();
+
+    auto writable_stream = MUST_OR_THROW_OOM(realm.heap().allocate<WritableStream>(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<void> WritableStream::initialize(JS::Realm& realm)
+{
+    MUST_OR_THROW_OOM(Base::initialize(realm));
+    set_prototype(&Bindings::ensure_web_prototype<Bindings::WritableStreamPrototype>(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);
+}
+
+}

+ 132 - 0
Userland/Libraries/LibWeb/Streams/WritableStream.h

@@ -0,0 +1,132 @@
+/*
+ * Copyright (c) 2023, Matthew Olsson <mattco@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Forward.h>
+#include <AK/SinglyLinkedList.h>
+#include <LibJS/Forward.h>
+#include <LibWeb/Bindings/PlatformObject.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/WebIDL/Promise.h>
+
+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<WebIDL::Promise> 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<JS::NonnullGCPtr<WritableStream>> construct_impl(JS::Realm& realm, Optional<JS::Handle<JS::Object>> 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<WebIDL::Promise const> close_request() const { return m_close_request; }
+    void set_close_request(JS::GCPtr<WebIDL::Promise> value) { m_close_request = value; }
+
+    JS::GCPtr<JS::Object> controller() { return m_controller; }
+    void set_controller(JS::GCPtr<JS::Object> value) { m_controller = value; }
+
+    JS::GCPtr<WebIDL::Promise const> in_flight_write_request() const { return m_in_flight_write_request; }
+    void set_in_flight_write_request(JS::GCPtr<WebIDL::Promise> value) { m_in_flight_write_request = value; }
+
+    JS::GCPtr<WebIDL::Promise const> in_flight_close_request() const { return m_in_flight_close_request; }
+    void set_in_flight_close_request(JS::GCPtr<WebIDL::Promise> value) { m_in_flight_close_request = value; }
+
+    Optional<PendingAbortRequest>& pending_abort_request() { return m_pending_abort_request; }
+    void set_pending_abort_request(Optional<PendingAbortRequest>&& 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<JS::Object const> writer() const { return m_writer; }
+    void set_writer(JS::GCPtr<JS::Object> value) { m_writer = value; }
+
+    SinglyLinkedList<JS::NonnullGCPtr<WebIDL::Promise>>& write_requests() { return m_write_requests; }
+
+private:
+    explicit WritableStream(JS::Realm&);
+
+    virtual JS::ThrowCompletionOr<void> 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<WebIDL::Promise> 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<JS::Object> 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<WebIDL::Promise> 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<WebIDL::Promise> m_in_flight_close_request;
+
+    // https://streams.spec.whatwg.org/#writablestream-pendingabortrequest
+    // A pending abort request
+    Optional<PendingAbortRequest> 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<JS::Object> 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<JS::NonnullGCPtr<WebIDL::Promise>> m_write_requests;
+};
+
+}

+ 12 - 0
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<undefined> abort(optional any reason);
+    // Promise<undefined> close();
+    // WritableStreamDefaultWriter getWriter();
+};

+ 1 - 0
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)