From 868cd9506923b6e47b56fdb0ec7cb1cc09031960 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Sat, 1 Apr 2023 19:53:28 -0700 Subject: [PATCH] LibWeb: Add the WritableStreamDefaultController --- Userland/Libraries/LibWeb/CMakeLists.txt | 1 + Userland/Libraries/LibWeb/Forward.h | 1 + .../LibWeb/Streams/AbstractOperations.cpp | 224 +++++++++++++++++- .../LibWeb/Streams/AbstractOperations.h | 13 + .../LibWeb/Streams/WritableStream.cpp | 1 + .../Libraries/LibWeb/Streams/WritableStream.h | 8 +- .../WritableStreamDefaultController.cpp | 46 ++++ .../Streams/WritableStreamDefaultController.h | 96 ++++++++ .../WritableStreamDefaultController.idl | 7 + .../Streams/WritableStreamDefaultWriter.h | 1 + 10 files changed, 393 insertions(+), 5 deletions(-) create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.cpp create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.h create mode 100644 Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.idl diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 73192be44e..e3c526cdad 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -470,6 +470,7 @@ set(SOURCES Streams/UnderlyingSink.cpp Streams/UnderlyingSource.cpp Streams/WritableStream.cpp + Streams/WritableStreamDefaultController.cpp Streams/WritableStreamDefaultWriter.cpp SVG/AttributeNames.cpp SVG/AttributeParser.cpp diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h index 7ad4f89d8a..e76773a934 100644 --- a/Userland/Libraries/LibWeb/Forward.h +++ b/Userland/Libraries/LibWeb/Forward.h @@ -433,6 +433,7 @@ class ReadRequest; struct UnderlyingSink; struct UnderlyingSource; class WritableStream; +class WritableStreamDefaultController; class WritableStreamDefaultWriter; } diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp index c7d633350a..1655e18f2d 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -834,6 +835,186 @@ bool writable_stream_close_queued_or_in_flight(WritableStream const& stream) return true; } +// https://streams.spec.whatwg.org/#writable-stream-finish-erroring +WebIDL::ExceptionOr writable_stream_finish_erroring(WritableStream& stream) +{ + auto& realm = stream.realm(); + + // 1. Assert: stream.[[state]] is "erroring". + VERIFY(stream.state() == WritableStream::State::Erroring); + + // 2. Assert: ! WritableStreamHasOperationMarkedInFlight(stream) is false. + VERIFY(!writable_stream_has_operation_marked_in_flight(stream)); + + // 3. Set stream.[[state]] to "errored". + stream.set_state(WritableStream::State::Errored); + + // 4. Perform ! stream.[[controller]].[[ErrorSteps]](). + stream.controller()->error_steps(); + + // 5. Let storedError be stream.[[storedError]]. + auto stored_error = stream.stored_error(); + + // 6. For each writeRequest of stream.[[writeRequests]]: + for (auto& write_request : stream.write_requests()) { + // 1. Reject writeRequest with storedError. + WebIDL::reject_promise(realm, *write_request, stored_error); + } + + // 7. Set stream.[[writeRequests]] to an empty list. + stream.write_requests().clear(); + + // 8. If stream.[[pendingAbortRequest]] is undefined, + if (!stream.pending_abort_request().has_value()) { + // 1. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); + + // 2. Return. + return {}; + } + + // 9. Let abortRequest be stream.[[pendingAbortRequest]]. + // 10. Set stream.[[pendingAbortRequest]] to undefined. + auto abort_request = stream.pending_abort_request().release_value(); + + // 11. If abortRequest’s was already erroring is true, + if (abort_request.was_already_erroring) { + // 1. Reject abortRequest’s promise with storedError. + WebIDL::reject_promise(realm, abort_request.promise, stored_error); + + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); + + // 3. Return. + return {}; + } + + // 12. Let promise be ! stream.[[controller]].[[AbortSteps]](abortRequest’s reason). + auto promise = TRY(stream.controller()->abort_steps(abort_request.reason)); + + // 13. Upon fulfillment of promise, + WebIDL::upon_fulfillment(*promise, [&, abort_promise = abort_request.promise](auto const&) -> WebIDL::ExceptionOr { + // 1. Resolve abortRequest’s promise with undefined. + WebIDL::resolve_promise(realm, abort_promise, JS::js_undefined()); + + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); + + return JS::js_undefined(); + }); + + // 14. Upon rejection of promise with reason reason, + WebIDL::upon_rejection(*promise, [&, abort_promise = abort_request.promise](auto const& reason) -> WebIDL::ExceptionOr { + // 1. Reject abortRequest’s promise with reason. + WebIDL::reject_promise(realm, abort_promise, reason); + + // 2. Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + writable_stream_reject_close_and_closed_promise_if_needed(stream); + + return JS::js_undefined(); + }); + + return {}; +} + +// https://streams.spec.whatwg.org/#writable-stream-has-operation-marked-in-flight +bool writable_stream_has_operation_marked_in_flight(WritableStream const& stream) +{ + // 1. If stream.[[inFlightWriteRequest]] is undefined and stream.[[inFlightCloseRequest]] is undefined, return false. + if (!stream.in_flight_write_request() && !stream.in_flight_close_request()) + return false; + + // 2. Return true. + return true; +} + +// https://streams.spec.whatwg.org/#writable-stream-reject-close-and-closed-promise-if-needed +void writable_stream_reject_close_and_closed_promise_if_needed(WritableStream& stream) +{ + auto& realm = stream.realm(); + + // 1. Assert: stream.[[state]] is "errored". + VERIFY(stream.state() == WritableStream::State::Errored); + + // 2. If stream.[[closeRequest]] is not undefined, + if (stream.close_request()) { + // 1. Assert: stream.[[inFlightCloseRequest]] is undefined. + VERIFY(!stream.in_flight_close_request()); + + // 2. Reject stream.[[closeRequest]] with stream.[[storedError]]. + WebIDL::reject_promise(realm, *stream.close_request(), stream.stored_error()); + + // 3. Set stream.[[closeRequest]] to undefined. + stream.set_close_request({}); + } + + // 3. Let writer be stream.[[writer]]. + auto writer = stream.writer(); + + // 4. If writer is not undefined, + if (writer) { + // 1. Reject writer.[[closedPromise]] with stream.[[storedError]]. + WebIDL::reject_promise(realm, *writer->closed_promise(), stream.stored_error()); + + // 2. Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + WebIDL::mark_promise_as_handled(*writer->closed_promise()); + } +} + +// https://streams.spec.whatwg.org/#writable-stream-start-erroring +WebIDL::ExceptionOr writable_stream_start_erroring(WritableStream& stream, JS::Value reason) +{ + // 1. Assert: stream.[[storedError]] is undefined. + VERIFY(stream.stored_error().is_undefined()); + + // 2. Assert: stream.[[state]] is "writable". + VERIFY(stream.state() == WritableStream::State::Writable); + + // 3. Let controller be stream.[[controller]]. + auto controller = stream.controller(); + + // 4. Assert: controller is not undefined. + VERIFY(controller); + + // 5. Set stream.[[state]] to "erroring". + stream.set_state(WritableStream::State::Erroring); + + // 6. Set stream.[[storedError]] to reason. + stream.set_stored_error(reason); + + // 7. Let writer be stream.[[writer]]. + auto writer = stream.writer(); + + // 8. If writer is not undefined, perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason). + if (writer) + writable_stream_default_writer_ensure_ready_promise_rejected(*writer, reason); + + // 9. If ! WritableStreamHasOperationMarkedInFlight(stream) is false and controller.[[started]] is true, perform ! WritableStreamFinishErroring(stream). + if (!writable_stream_has_operation_marked_in_flight(stream) && controller->started()) + TRY(writable_stream_finish_erroring(stream)); + + return {}; +} + +// https://streams.spec.whatwg.org/#writable-stream-default-writer-ensure-ready-promise-rejected +void writable_stream_default_writer_ensure_ready_promise_rejected(WritableStreamDefaultWriter& writer, JS::Value error) +{ + auto& realm = writer.realm(); + + // 1. If writer.[[readyPromise]].[[PromiseState]] is "pending", reject writer.[[readyPromise]] with error. + auto& ready_promise = verify_cast(*writer.ready_promise()->promise()); + if (ready_promise.state() == JS::Promise::State::Pending) { + WebIDL::reject_promise(realm, *writer.ready_promise(), error); + } + // 2. Otherwise, set writer.[[readyPromise]] to a promise rejected with error. + else { + writer.set_ready_promise(WebIDL::create_rejected_promise(realm, error)); + } + + // 3. Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + WebIDL::mark_promise_as_handled(*writer.ready_promise()); +} + // https://streams.spec.whatwg.org/#writable-stream-default-writer-get-desired-size Optional writable_stream_default_writer_get_desired_size(WritableStreamDefaultWriter const& writer) { @@ -851,8 +1032,47 @@ Optional writable_stream_default_writer_get_desired_size(WritableStreamD if (state == WritableStream::State::Closed) return 0.0; - // FIXME: 5. Return ! WritableStreamDefaultControllerGetDesiredSize(stream.[[controller]]). - return 0.0; + // 5. Return ! WritableStreamDefaultControllerGetDesiredSize(stream.[[controller]]). + return writable_stream_default_controller_get_desired_size(*stream->controller()); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-clear-algorithms +void writable_stream_default_controller_clear_algorithms(WritableStreamDefaultController& controller) +{ + // 1. Set controller.[[writeAlgorithm]] to undefined. + controller.set_write_algorithm({}); + + // 2. Set controller.[[closeAlgorithm]] to undefined. + controller.set_close_algorithm({}); + + // 3. Set controller.[[abortAlgorithm]] to undefined. + controller.set_abort_algorithm({}); + + // 4. Set controller.[[strategySizeAlgorithm]] to undefined. + controller.set_strategy_size_algorithm({}); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-error +WebIDL::ExceptionOr writable_stream_default_controller_error(WritableStreamDefaultController& controller, JS::Value error) +{ + // 1. Let stream be controller.[[stream]]. + auto stream = controller.stream(); + + // 2. Assert: stream.[[state]] is "writable". + VERIFY(stream->state() == WritableStream::State::Writable); + + // 3. Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + writable_stream_default_controller_clear_algorithms(controller); + + // 4. Perform ! WritableStreamStartErroring(stream, error). + return writable_stream_start_erroring(stream, error); +} + +// https://streams.spec.whatwg.org/#writable-stream-default-controller-get-desired-size +double writable_stream_default_controller_get_desired_size(WritableStreamDefaultController const& controller) +{ + // 1. Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. + return controller.strategy_hwm() - controller.queue_total_size(); } // Non-standard function to aid in converting a user-provided function into a WebIDL::Callback. This is essentially diff --git a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h index 3b77dc9226..1e69c9dbe3 100644 --- a/Userland/Libraries/LibWeb/Streams/AbstractOperations.h +++ b/Userland/Libraries/LibWeb/Streams/AbstractOperations.h @@ -18,6 +18,9 @@ using SizeAlgorithm = JS::SafeFunction; using PullAlgorithm = JS::SafeFunction>()>; using CancelAlgorithm = JS::SafeFunction>(JS::Value)>; using StartAlgorithm = JS::SafeFunction>()>; +using AbortAlgorithm = JS::SafeFunction>(JS::Value)>; +using CloseAlgorithm = JS::SafeFunction>()>; +using WriteAlgorithm = JS::SafeFunction>(JS::Value)>; WebIDL::ExceptionOr> acquire_readable_stream_default_reader(ReadableStream&); bool is_readable_stream_locked(ReadableStream const&); @@ -54,8 +57,18 @@ bool is_writable_stream_locked(WritableStream const&); WebIDL::ExceptionOr set_up_writable_stream_default_writer(WritableStreamDefaultWriter&, WritableStream&); bool writable_stream_close_queued_or_in_flight(WritableStream const&); +WebIDL::ExceptionOr writable_stream_finish_erroring(WritableStream&); +bool writable_stream_has_operation_marked_in_flight(WritableStream const&); +void writable_stream_reject_close_and_closed_promise_if_needed(WritableStream&); +WebIDL::ExceptionOr writable_stream_start_erroring(WritableStream&, JS::Value reason); Optional writable_stream_default_writer_get_desired_size(WritableStreamDefaultWriter const&); +void writable_stream_default_writer_ensure_ready_promise_rejected(WritableStreamDefaultWriter&, JS::Value error); +Optional writable_stream_default_writer_get_desired_size(WritableStreamDefaultWriter const&); + +void writable_stream_default_controller_clear_algorithms(WritableStreamDefaultController&); +WebIDL::ExceptionOr writable_stream_default_controller_error(WritableStreamDefaultController&, JS::Value error); +double writable_stream_default_controller_get_desired_size(WritableStreamDefaultController const&); JS::ThrowCompletionOr> property_to_callback(JS::VM& vm, JS::Value value, JS::PropertyKey const& property_key); diff --git a/Userland/Libraries/LibWeb/Streams/WritableStream.cpp b/Userland/Libraries/LibWeb/Streams/WritableStream.cpp index b277ffcbbb..898da2c62a 100644 --- a/Userland/Libraries/LibWeb/Streams/WritableStream.cpp +++ b/Userland/Libraries/LibWeb/Streams/WritableStream.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include diff --git a/Userland/Libraries/LibWeb/Streams/WritableStream.h b/Userland/Libraries/LibWeb/Streams/WritableStream.h index 6486f5874d..10b333ce10 100644 --- a/Userland/Libraries/LibWeb/Streams/WritableStream.h +++ b/Userland/Libraries/LibWeb/Streams/WritableStream.h @@ -54,8 +54,9 @@ public: 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 controller() const { return m_controller; } + 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; } @@ -73,6 +74,7 @@ public: void set_stored_error(JS::Value value) { m_stored_error = value; } JS::GCPtr writer() const { return m_writer; } + JS::GCPtr writer() { return m_writer; } void set_writer(JS::GCPtr value) { m_writer = value; } SinglyLinkedList>& write_requests() { return m_write_requests; } @@ -94,7 +96,7 @@ private: // 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; + JS::GCPtr m_controller; // https://streams.spec.whatwg.org/#writablestream-detached // A boolean flag set to true when the stream is transferred diff --git a/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.cpp b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.cpp new file mode 100644 index 0000000000..880434ecab --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.cpp @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#ws-default-controller-error +WebIDL::ExceptionOr WritableStreamDefaultController::error(JS::Value error) +{ + // 1. Let state be this.[[stream]].[[state]]. + auto state = m_stream->state(); + + // 2. If state is not "writable", return. + if (state != WritableStream::State::Writable) + return {}; + + // 3. Perform ! WritableStreamDefaultControllerError(this, e). + return writable_stream_default_controller_error(*this, error); +} + +// https://streams.spec.whatwg.org/#ws-default-controller-private-abort +WebIDL::ExceptionOr> WritableStreamDefaultController::abort_steps(JS::Value reason) +{ + // 1. Let result be the result of performing this.[[abortAlgorithm]], passing reason. + auto result = TRY((*m_abort_algorithm)(reason)); + + // 2. Perform ! WritableStreamDefaultControllerClearAlgorithms(this). + writable_stream_default_controller_clear_algorithms(*this); + + // 3. Return result. + return result; +} + +// https://streams.spec.whatwg.org/#ws-default-controller-private-error +void WritableStreamDefaultController::error_steps() +{ + // 1. Perform ! ResetQueue(this). + reset_queue(*this); +} + +} diff --git a/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.h b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.h new file mode 100644 index 0000000000..a86e3711b8 --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.h @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023, Matthew Olsson + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#writablestreamdefaultcontroller +class WritableStreamDefaultController final : public Bindings::PlatformObject { + WEB_PLATFORM_OBJECT(WritableStreamDefaultController, Bindings::PlatformObject); + +public: + virtual ~WritableStreamDefaultController() override = default; + + WebIDL::ExceptionOr error(JS::Value error); + JS::NonnullGCPtr signal() { return *m_signal; } + + auto& abort_algorithm() { return m_abort_algorithm; } + void set_abort_algorithm(Optional&& value) { m_abort_algorithm = move(value); } + + auto& close_algorithm() { return m_close_algorithm; } + void set_close_algorithm(Optional&& value) { m_close_algorithm = move(value); } + + SinglyLinkedList& queue() { return m_queue; } + + double queue_total_size() const { return m_queue_total_size; } + void set_queue_total_size(double value) { m_queue_total_size = value; } + + bool started() const { return m_started; } + void set_started(bool value) { m_started = value; } + + size_t strategy_hwm() const { return m_strategy_hwm; } + void set_strategy_hwm(size_t value) { m_strategy_hwm = value; } + + auto& strategy_size_algorithm() { return m_strategy_size_algorithm; } + void set_strategy_size_algorithm(Optional&& value) { m_strategy_size_algorithm = move(value); } + + JS::NonnullGCPtr stream() { return *m_stream; } + void set_stream(JS::NonnullGCPtr value) { m_stream = value; } + + auto& write_algorithm() { return m_write_algorithm; } + void set_write_algorithm(Optional&& value) { m_write_algorithm = move(value); } + + WebIDL::ExceptionOr> abort_steps(JS::Value reason); + void error_steps(); + +private: + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-abortalgorithm + // A promise-returning algorithm, taking one argument (the abort reason), which communicates a requested abort to the underlying sink + Optional m_abort_algorithm; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-closealgorithm + // A promise-returning algorithm which communicates a requested close to the underlying sink + Optional m_close_algorithm; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-queue + // A list representing the stream’s internal queue of chunks + SinglyLinkedList m_queue; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-queuetotalsize + // The total size of all the chunks stored in [[queue]] + double m_queue_total_size { 0 }; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-signal + // An AbortSignal that can be used to abort the pending write or close operation when the stream is aborted. + JS::GCPtr m_signal; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-started + // A boolean flag indicating whether the underlying sink has finished starting + bool m_started { false }; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategyhwm + // A number supplied by the creator of the stream as part of the stream’s queuing strategy, indicating the point at which the stream will apply backpressure to its underlying sink + size_t m_strategy_hwm { 0 }; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-strategysizealgorithm + // An algorithm to calculate the size of enqueued chunks, as part of the stream’s queuing strategy + Optional m_strategy_size_algorithm; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-stream + // The WritableStream instance controlled + JS::GCPtr m_stream; + + // https://streams.spec.whatwg.org/#writablestreamdefaultcontroller-writealgorithm + // A promise-returning algorithm, taking one argument (the chunk to write), which writes data to the underlying sink + Optional m_write_algorithm; +}; + +} diff --git a/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.idl b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.idl new file mode 100644 index 0000000000..a2a93b04c1 --- /dev/null +++ b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultController.idl @@ -0,0 +1,7 @@ +#import + +[Exposed=*] +interface WritableStreamDefaultController { + readonly attribute AbortSignal signal; + undefined error(optional any e); +}; diff --git a/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultWriter.h b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultWriter.h index dff7c269c3..a11b1be596 100644 --- a/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultWriter.h +++ b/Userland/Libraries/LibWeb/Streams/WritableStreamDefaultWriter.h @@ -35,6 +35,7 @@ public: void set_ready_promise(JS::GCPtr value) { m_ready_promise = value; } JS::GCPtr stream() const { return m_stream; } + JS::GCPtr stream() { return m_stream; } void set_stream(JS::GCPtr value) { m_stream = value; } private: