diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index eba2db98d25..2bdc3005e98 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -743,6 +743,7 @@ set(SOURCES Streams/GenericTransformStream.cpp Streams/ReadableByteStreamController.cpp Streams/ReadableStream.cpp + Streams/ReadableStreamAsyncIterator.cpp Streams/ReadableStreamBYOBReader.cpp Streams/ReadableStreamBYOBRequest.cpp Streams/ReadableStreamDefaultController.cpp diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 0271a0c1d39..7e554fe22f9 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -786,6 +786,7 @@ class ByteLengthQueuingStrategy; class CountQueuingStrategy; class ReadableByteStreamController; class ReadableStream; +class ReadableStreamAsyncIterator; class ReadableStreamBYOBReader; class ReadableStreamBYOBRequest; class ReadableStreamDefaultController; diff --git a/Libraries/LibWeb/Streams/ReadableStream.idl b/Libraries/LibWeb/Streams/ReadableStream.idl index e21898e637f..694624e8ab4 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.idl +++ b/Libraries/LibWeb/Streams/ReadableStream.idl @@ -24,8 +24,13 @@ dictionary ReadableStreamGetReaderOptions { ReadableStreamReaderMode mode; }; +// https://streams.spec.whatwg.org/#dictdef-readablestreamiteratoroptions +dictionary ReadableStreamIteratorOptions { + boolean preventCancel = false; +}; + // https://streams.spec.whatwg.org/#readablestream -[Exposed=*, Transferable] +[Exposed=*, Transferable, DefinesAsyncIteratorReturn] interface ReadableStream { constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); @@ -39,7 +44,7 @@ interface ReadableStream { Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); sequence tee(); - // FIXME: async iterable(optional ReadableStreamIteratorOptions options = {}); + async iterable(optional ReadableStreamIteratorOptions options = {}); }; typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; diff --git a/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.cpp b/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.cpp new file mode 100644 index 00000000000..09438c4b0c3 --- /dev/null +++ b/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::Bindings { + +template<> +void Intrinsics::create_web_prototype_and_constructor(JS::Realm& realm) +{ + auto prototype = realm.create(realm); + m_prototypes.set("ReadableStreamAsyncIterator"_fly_string, prototype); +} + +} + +namespace Web::Streams { + +GC_DEFINE_ALLOCATOR(ReadableStreamAsyncIterator); + +// https://streams.spec.whatwg.org/#ref-for-asynchronous-iterator-initialization-steps +WebIDL::ExceptionOr> ReadableStreamAsyncIterator::create(JS::Realm& realm, JS::Object::PropertyKind kind, ReadableStream& stream, ReadableStreamIteratorOptions options) +{ + // 1. Let reader be ? AcquireReadableStreamDefaultReader(stream). + // 2. Set iterator’s reader to reader. + auto reader = TRY(acquire_readable_stream_default_reader(stream)); + + // 3. Let preventCancel be args[0]["preventCancel"]. + // 4. Set iterator’s prevent cancel to preventCancel. + auto prevent_cancel = options.prevent_cancel; + + return realm.create(realm, kind, reader, prevent_cancel); +} + +ReadableStreamAsyncIterator::ReadableStreamAsyncIterator(JS::Realm& realm, JS::Object::PropertyKind kind, GC::Ref reader, bool prevent_cancel) + : AsyncIterator(realm, kind) + , m_reader(reader) + , m_prevent_cancel(prevent_cancel) +{ +} + +ReadableStreamAsyncIterator::~ReadableStreamAsyncIterator() = default; + +void ReadableStreamAsyncIterator::initialize(JS::Realm& realm) +{ + Base::initialize(realm); + WEB_SET_PROTOTYPE_FOR_INTERFACE(ReadableStreamAsyncIterator); +} + +void ReadableStreamAsyncIterator::visit_edges(JS::Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_reader); +} + +class ReadableStreamAsyncIteratorReadRequest final : public ReadRequest { + GC_CELL(ReadableStreamAsyncIteratorReadRequest, ReadRequest); + GC_DECLARE_ALLOCATOR(ReadableStreamAsyncIteratorReadRequest); + +public: + ReadableStreamAsyncIteratorReadRequest(JS::Realm& realm, ReadableStreamDefaultReader& reader, WebIDL::Promise& promise) + : m_realm(realm) + , m_reader(reader) + , m_promise(promise) + { + } + + // chunk steps, given chunk + virtual void on_chunk(JS::Value chunk) override + { + // 1. Resolve promise with chunk. + WebIDL::resolve_promise(m_realm, m_promise, chunk); + } + + // close steps + virtual void on_close() override + { + // 1. Perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 2. Resolve promise with end of iteration. + WebIDL::resolve_promise(m_realm, m_promise, JS::js_special_empty_value()); + } + + // error steps, given e + virtual void on_error(JS::Value error) override + { + // 1. Perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 2. Reject promise with e. + WebIDL::reject_promise(m_realm, m_promise, error); + } + +private: + virtual void visit_edges(Visitor& visitor) override + { + Base::visit_edges(visitor); + visitor.visit(m_realm); + visitor.visit(m_reader); + visitor.visit(m_promise); + } + + GC::Ref m_realm; + GC::Ref m_reader; + GC::Ref m_promise; +}; + +GC_DEFINE_ALLOCATOR(ReadableStreamAsyncIteratorReadRequest); + +// https://streams.spec.whatwg.org/#ref-for-dfn-get-the-next-iteration-result +GC::Ref ReadableStreamAsyncIterator::next_iteration_result(JS::Realm& realm) +{ + // 1. Let reader be iterator’s reader. + // 2. Assert: reader.[[stream]] is not undefined. + VERIFY(m_reader->stream()); + + // 3. Let promise be a new promise. + auto promise = WebIDL::create_promise(realm); + + // 4. Let readRequest be a new read request with the following items: + auto read_request = heap().allocate(realm, m_reader, promise); + + // 5. Perform ! ReadableStreamDefaultReaderRead(this, readRequest). + readable_stream_default_reader_read(m_reader, read_request); + + // 6. Return promise. + return promise; +} + +// https://streams.spec.whatwg.org/#ref-for-asynchronous-iterator-return +GC::Ref ReadableStreamAsyncIterator::iterator_return(JS::Realm& realm, JS::Value arg) +{ + // 1. Let reader be iterator’s reader. + // 2. Assert: reader.[[stream]] is not undefined. + VERIFY(m_reader->stream()); + + // 3. Assert: reader.[[readRequests]] is empty, as the async iterator machinery guarantees that any previous calls + // to next() have settled before this is called. + VERIFY(m_reader->read_requests().is_empty()); + + // 4. If iterator’s prevent cancel is false: + if (!m_prevent_cancel) { + // 1. Let result be ! ReadableStreamReaderGenericCancel(reader, arg). + auto result = readable_stream_reader_generic_cancel(m_reader, arg); + + // 2. Perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 3. Return result. + return result; + } + + // 5. Perform ! ReadableStreamDefaultReaderRelease(reader). + readable_stream_default_reader_release(m_reader); + + // 6. Return a promise resolved with undefined. + return WebIDL::create_resolved_promise(realm, JS::js_undefined()); +} + +} diff --git a/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.h b/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.h new file mode 100644 index 00000000000..fe856368b78 --- /dev/null +++ b/Libraries/LibWeb/Streams/ReadableStreamAsyncIterator.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::Streams { + +// https://streams.spec.whatwg.org/#dictdef-readablestreamiteratoroptions +struct ReadableStreamIteratorOptions { + bool prevent_cancel { false }; +}; + +class ReadableStreamAsyncIterator final : public WebIDL::AsyncIterator { + WEB_PLATFORM_OBJECT(ReadableStreamAsyncIterator, WebIDL::AsyncIterator); + GC_DECLARE_ALLOCATOR(ReadableStreamAsyncIterator); + +public: + static WebIDL::ExceptionOr> create(JS::Realm&, JS::Object::PropertyKind, ReadableStream&, ReadableStreamIteratorOptions); + + virtual ~ReadableStreamAsyncIterator() override; + +private: + ReadableStreamAsyncIterator(JS::Realm&, JS::Object::PropertyKind, GC::Ref, bool prevent_cancel); + + virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Cell::Visitor&) override; + + virtual GC::Ref next_iteration_result(JS::Realm&) override; + virtual GC::Ref iterator_return(JS::Realm&, JS::Value) override; + + GC::Ref m_reader; + bool m_prevent_cancel { false }; +}; + +} diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 56b087accec..64e15e52480 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -297,7 +297,7 @@ libweb_js_bindings(ServiceWorker/ServiceWorkerRegistration) libweb_js_bindings(Streams/ByteLengthQueuingStrategy) libweb_js_bindings(Streams/CountQueuingStrategy) libweb_js_bindings(Streams/ReadableByteStreamController) -libweb_js_bindings(Streams/ReadableStream) +libweb_js_bindings(Streams/ReadableStream ASYNC_ITERABLE) libweb_js_bindings(Streams/ReadableStreamBYOBReader) libweb_js_bindings(Streams/ReadableStreamBYOBRequest) libweb_js_bindings(Streams/ReadableStreamDefaultController) diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/idlharness.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/idlharness.any.txt index 5f613f9b189..1bea1f3af26 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/idlharness.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/idlharness.any.txt @@ -2,8 +2,7 @@ Harness status: OK Found 229 tests -228 Pass -1 Fail +229 Pass Pass idl_test setup Pass idl_test validation Pass ReadableStreamDefaultReader includes ReadableStreamGenericReader: member names are unique @@ -21,7 +20,7 @@ Pass ReadableStream interface: operation getReader(optional ReadableStreamGetRea Pass ReadableStream interface: operation pipeThrough(ReadableWritablePair, optional StreamPipeOptions) Pass ReadableStream interface: operation pipeTo(WritableStream, optional StreamPipeOptions) Pass ReadableStream interface: operation tee() -Fail ReadableStream interface: async iterable +Pass ReadableStream interface: async iterable Pass ReadableStream must be primary interface of new ReadableStream() Pass Stringification of new ReadableStream() Pass ReadableStream interface: new ReadableStream() must inherit property "from(any)" with the proper type diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-streams/async-iterator.any.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-streams/async-iterator.any.txt index 29fb632a93e..ad69a055969 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/streams/readable-streams/async-iterator.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/readable-streams/async-iterator.any.txt @@ -2,45 +2,46 @@ Harness status: OK Found 41 tests -41 Fail -Fail Async iterator instances should have the correct list of properties -Fail Async-iterating a push source -Fail Async-iterating a pull source -Fail Async-iterating a push source with undefined values -Fail Async-iterating a pull source with undefined values -Fail Async-iterating a pull source manually -Fail Async-iterating an errored stream throws -Fail Async-iterating a closed stream never executes the loop body, but works fine -Fail Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function -Fail Async-iterating a partially consumed stream +36 Pass +5 Fail +Pass Async iterator instances should have the correct list of properties +Pass Async-iterating a push source +Pass Async-iterating a pull source +Pass Async-iterating a push source with undefined values +Pass Async-iterating a pull source with undefined values +Pass Async-iterating a pull source manually +Pass Async-iterating an errored stream throws +Pass Async-iterating a closed stream never executes the loop body, but works fine +Pass Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function +Pass Async-iterating a partially consumed stream Fail Cancellation behavior when throwing inside loop body; preventCancel = false -Fail Cancellation behavior when throwing inside loop body; preventCancel = true +Pass Cancellation behavior when throwing inside loop body; preventCancel = true Fail Cancellation behavior when breaking inside loop body; preventCancel = false -Fail Cancellation behavior when breaking inside loop body; preventCancel = true +Pass Cancellation behavior when breaking inside loop body; preventCancel = true Fail Cancellation behavior when returning inside loop body; preventCancel = false -Fail Cancellation behavior when returning inside loop body; preventCancel = true -Fail Cancellation behavior when manually calling return(); preventCancel = false -Fail Cancellation behavior when manually calling return(); preventCancel = true -Fail next() rejects if the stream errors -Fail return() does not rejects if the stream has not errored yet -Fail return() rejects if the stream has errored -Fail next() that succeeds; next() that reports an error; next() -Fail next() that succeeds; next() that reports an error(); next() [no awaiting] -Fail next() that succeeds; next() that reports an error(); return() -Fail next() that succeeds; next() that reports an error(); return() [no awaiting] -Fail next() that succeeds; return() -Fail next() that succeeds; return() [no awaiting] -Fail return(); next() -Fail return(); next() [no awaiting] -Fail return(); next() with delayed cancel() -Fail return(); next() with delayed cancel() [no awaiting] -Fail return(); return() -Fail return(); return() [no awaiting] -Fail values() throws if there's already a lock -Fail Acquiring a reader after exhaustively async-iterating a stream -Fail Acquiring a reader after return()ing from a stream that errors +Pass Cancellation behavior when returning inside loop body; preventCancel = true +Pass Cancellation behavior when manually calling return(); preventCancel = false +Pass Cancellation behavior when manually calling return(); preventCancel = true +Pass next() rejects if the stream errors +Pass return() does not rejects if the stream has not errored yet +Pass return() rejects if the stream has errored +Pass next() that succeeds; next() that reports an error; next() +Pass next() that succeeds; next() that reports an error(); next() [no awaiting] +Pass next() that succeeds; next() that reports an error(); return() +Pass next() that succeeds; next() that reports an error(); return() [no awaiting] +Pass next() that succeeds; return() +Pass next() that succeeds; return() [no awaiting] +Pass return(); next() +Pass return(); next() [no awaiting] +Pass return(); next() with delayed cancel() +Pass return(); next() with delayed cancel() [no awaiting] +Pass return(); return() +Pass return(); return() [no awaiting] +Pass values() throws if there's already a lock +Pass Acquiring a reader after exhaustively async-iterating a stream +Pass Acquiring a reader after return()ing from a stream that errors Fail Acquiring a reader after partially async-iterating a stream Fail Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true -Fail return() should unlock the stream synchronously when preventCancel = false -Fail return() should unlock the stream synchronously when preventCancel = true -Fail close() while next() is pending \ No newline at end of file +Pass return() should unlock the stream synchronously when preventCancel = false +Pass return() should unlock the stream synchronously when preventCancel = true +Pass close() while next() is pending \ No newline at end of file