diff --git a/Libraries/LibWeb/HTML/StructuredSerialize.cpp b/Libraries/LibWeb/HTML/StructuredSerialize.cpp index 498a2a6c9c3..0cd43016aab 100644 --- a/Libraries/LibWeb/HTML/StructuredSerialize.cpp +++ b/Libraries/LibWeb/HTML/StructuredSerialize.cpp @@ -49,6 +49,7 @@ #include #include #include +#include #include #include @@ -1293,7 +1294,8 @@ static bool is_interface_exposed_on_target_realm(TransferType name, JS::Realm& r switch (name) { case TransferType::MessagePort: return intrinsics.is_exposed("MessagePort"sv); - break; + case TransferType::ReadableStream: + return intrinsics.is_exposed("ReadableStream"sv); case TransferType::Unknown: dbgln("Unknown interface type for transfer: {}", to_underlying(name)); break; @@ -1311,6 +1313,11 @@ static WebIDL::ExceptionOr> create_transferred TRY(message_port->transfer_receiving_steps(transfer_data_holder)); return message_port; } + case TransferType::ReadableStream: { + auto readable_stream = target_realm.create(target_realm); + TRY(readable_stream->transfer_receiving_steps(transfer_data_holder)); + return readable_stream; + } case TransferType::ArrayBuffer: case TransferType::ResizableArrayBuffer: dbgln("ArrayBuffer ({}) is not a platform object.", to_underlying(name)); diff --git a/Libraries/LibWeb/HTML/StructuredSerialize.h b/Libraries/LibWeb/HTML/StructuredSerialize.h index cfa61af8f21..7d3ca04a946 100644 --- a/Libraries/LibWeb/HTML/StructuredSerialize.h +++ b/Libraries/LibWeb/HTML/StructuredSerialize.h @@ -49,6 +49,7 @@ enum class TransferType : u8 { MessagePort = 1, ArrayBuffer = 2, ResizableArrayBuffer = 3, + ReadableStream = 4, }; WebIDL::ExceptionOr structured_serialize(JS::VM& vm, JS::Value); diff --git a/Libraries/LibWeb/Streams/ReadableStream.cpp b/Libraries/LibWeb/Streams/ReadableStream.cpp index 4c61f264a73..285c3a4319a 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.cpp +++ b/Libraries/LibWeb/Streams/ReadableStream.cpp @@ -11,6 +11,8 @@ #include #include #include +#include +#include #include #include #include @@ -89,6 +91,22 @@ ReadableStream::ReadableStream(JS::Realm& realm) ReadableStream::~ReadableStream() = default; +void ReadableStream::initialize(JS::Realm& realm) +{ + WEB_SET_PROTOTYPE_FOR_INTERFACE(ReadableStream); + Base::initialize(realm); +} + +void ReadableStream::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + if (m_controller.has_value()) + m_controller->visit([&](auto& controller) { visitor.visit(controller); }); + visitor.visit(m_stored_error); + if (m_reader.has_value()) + m_reader->visit([&](auto& reader) { visitor.visit(reader); }); +} + // https://streams.spec.whatwg.org/#rs-locked bool ReadableStream::locked() const { @@ -218,22 +236,6 @@ void ReadableStream::error(JS::Value error) }); } -void ReadableStream::initialize(JS::Realm& realm) -{ - WEB_SET_PROTOTYPE_FOR_INTERFACE(ReadableStream); - Base::initialize(realm); -} - -void ReadableStream::visit_edges(Cell::Visitor& visitor) -{ - Base::visit_edges(visitor); - if (m_controller.has_value()) - m_controller->visit([&](auto& controller) { visitor.visit(controller); }); - visitor.visit(m_stored_error); - if (m_reader.has_value()) - m_reader->visit([&](auto& reader) { visitor.visit(reader); }); -} - // https://streams.spec.whatwg.org/#readablestream-locked bool ReadableStream::is_readable() const { @@ -453,4 +455,62 @@ GC::Ref ReadableStream::piped_through(GC::Ref t return transform->readable(); } +// https://streams.spec.whatwg.org/#ref-for-transfer-steps +WebIDL::ExceptionOr ReadableStream::transfer_steps(HTML::TransferDataHolder& data_holder) +{ + auto& realm = this->realm(); + auto& vm = realm.vm(); + + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. If ! IsReadableStreamLocked(value) is true, throw a "DataCloneError" DOMException. + if (is_readable_stream_locked(*this)) + return WebIDL::DataCloneError::create(realm, "Cannot transfer locked ReadableStream"_string); + + // 2. Let port1 be a new MessagePort in the current Realm. + auto port1 = HTML::MessagePort::create(realm); + + // 3. Let port2 be a new MessagePort in the current Realm. + auto port2 = HTML::MessagePort::create(realm, HTML::TransferType::ReadableStream); + + // 4. Entangle port1 and port2. + port1->entangle_with(port2); + + // 5. Let writable be a new WritableStream in the current Realm. + auto writable = realm.create(realm); + + // 6. Perform ! SetUpCrossRealmTransformWritable(writable, port1). + set_up_cross_realm_transform_writable(realm, writable, port1); + + // 7. Let promise be ! ReadableStreamPipeTo(value, writable, false, false, false). + auto promise = readable_stream_pipe_to(*this, writable, false, false, false); + + // 8. Set promise.[[PromiseIsHandled]] to true. + WebIDL::mark_promise_as_handled(promise); + + // 9. Set dataHolder.[[port]] to ! StructuredSerializeWithTransfer(port2, « port2 »). + auto result = MUST(HTML::structured_serialize_with_transfer(vm, port2, { { GC::Root { port2 } } })); + data_holder = move(result.transfer_data_holders.first()); + + return {}; +} + +// https://streams.spec.whatwg.org/#ref-for-transfer-receiving-steps +WebIDL::ExceptionOr ReadableStream::transfer_receiving_steps(HTML::TransferDataHolder& data_holder) +{ + auto& realm = this->realm(); + + HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes }; + + // 1. Let deserializedRecord be ! StructuredDeserializeWithTransfer(dataHolder.[[port]], the current Realm). + // 2. Let port be deserializedRecord.[[Deserialized]]. + auto port = HTML::MessagePort::create(realm); + TRY(port->transfer_receiving_steps(data_holder)); + + // 3. Perform ! SetUpCrossRealmTransformReadable(value, port). + set_up_cross_realm_transform_readable(realm, *this, port); + + return {}; +} + } diff --git a/Libraries/LibWeb/Streams/ReadableStream.h b/Libraries/LibWeb/Streams/ReadableStream.h index ebb63435e08..427e80f1765 100644 --- a/Libraries/LibWeb/Streams/ReadableStream.h +++ b/Libraries/LibWeb/Streams/ReadableStream.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +59,9 @@ struct ReadableStreamPair { }; // https://streams.spec.whatwg.org/#readablestream -class ReadableStream final : public Bindings::PlatformObject { +class ReadableStream final + : public Bindings::PlatformObject + , public Bindings::Transferable { WEB_PLATFORM_OBJECT(ReadableStream, Bindings::PlatformObject); GC_DECLARE_ALLOCATOR(ReadableStream); @@ -113,6 +116,11 @@ public: GC::Ptr current_byob_request_view(); + // ^Transferable + virtual WebIDL::ExceptionOr transfer_steps(HTML::TransferDataHolder&) override; + virtual WebIDL::ExceptionOr transfer_receiving_steps(HTML::TransferDataHolder&) override; + virtual HTML::TransferType primary_interface() const override { return HTML::TransferType::ReadableStream; } + private: explicit ReadableStream(JS::Realm&); @@ -123,10 +131,6 @@ private: // A ReadableStreamDefaultController or ReadableByteStreamController created with the ability to control the state and queue of this stream Optional m_controller; - // https://streams.spec.whatwg.org/#readablestream-detached - // A boolean flag set to true when the stream is transferred - bool m_detached { false }; - // https://streams.spec.whatwg.org/#readablestream-disturbed // A boolean flag set to true when the stream has been read from or canceled bool m_disturbed { false }; diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt index 2363c8a9128..b779d6328cb 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt @@ -2,9 +2,8 @@ Harness status: OK Found 150 tests -140 Pass +141 Pass 9 Fail -1 Optional Feature Unsupported Pass primitive undefined Pass primitive null Pass primitive true @@ -149,7 +148,7 @@ Pass A detached ArrayBuffer cannot be transferred Pass A detached platform object cannot be transferred Pass Transferring a non-transferable platform object fails Pass An object whose interface is deleted from the global object must still be received -Optional Feature Unsupported A subclass instance will be received as its closest transferable superclass +Pass A subclass instance will be received as its closest transferable superclass Pass Resizable ArrayBuffer is transferable Fail Length-tracking TypedArray is transferable Fail Length-tracking DataView is transferable diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/structured-clone/structured-clone.any.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/structured-clone/structured-clone.any.txt index 2363c8a9128..b779d6328cb 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/structured-clone/structured-clone.any.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/structured-clone/structured-clone.any.txt @@ -2,9 +2,8 @@ Harness status: OK Found 150 tests -140 Pass +141 Pass 9 Fail -1 Optional Feature Unsupported Pass primitive undefined Pass primitive null Pass primitive true @@ -149,7 +148,7 @@ Pass A detached ArrayBuffer cannot be transferred Pass A detached platform object cannot be transferred Pass Transferring a non-transferable platform object fails Pass An object whose interface is deleted from the global object must still be received -Optional Feature Unsupported A subclass instance will be received as its closest transferable superclass +Pass A subclass instance will be received as its closest transferable superclass Pass Resizable ArrayBuffer is transferable Fail Length-tracking TypedArray is transferable Fail Length-tracking DataView is transferable diff --git a/Tests/LibWeb/Text/expected/wpt-import/streams/transferable/readable-stream.txt b/Tests/LibWeb/Text/expected/wpt-import/streams/transferable/readable-stream.txt new file mode 100644 index 00000000000..f95a7a49eb5 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/streams/transferable/readable-stream.txt @@ -0,0 +1,21 @@ +Harness status: OK + +Found 16 tests + +16 Pass +Pass sending one chunk through a transferred stream should work +Pass sending ten chunks through a transferred stream should work +Pass sending ten chunks one at a time should work +Pass sending ten chunks on demand should work +Pass transferring a stream should relieve backpressure +Pass transferring a stream should add one chunk to the queue size +Pass the extra queue from transferring is counted in chunks +Pass cancel should be propagated to the original +Pass cancel should abort a pending read() +Pass stream cancel should not wait for underlying source cancel +Pass serialization should not happen until the value is read +Pass transferring a non-serializable chunk should error both sides +Pass errors should be passed through +Pass race between cancel() and error() should leave sides in different states +Pass race between cancel() and close() should be benign +Pass race between cancel() and enqueue() should be benign \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/transferable/readable-stream.html b/Tests/LibWeb/Text/input/wpt-import/streams/transferable/readable-stream.html new file mode 100644 index 00000000000..12edd0b61b7 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/transferable/readable-stream.html @@ -0,0 +1,260 @@ + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/streams/transferable/resources/helpers.js b/Tests/LibWeb/Text/input/wpt-import/streams/transferable/resources/helpers.js new file mode 100644 index 00000000000..12504537f91 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/streams/transferable/resources/helpers.js @@ -0,0 +1,132 @@ +'use strict'; + +(() => { + // Create a ReadableStream that will pass the tests in + // testTransferredReadableStream(), below. + function createOriginalReadableStream() { + return new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.close(); + } + }); + } + + // Common tests to roughly determine that |rs| is a correctly transferred + // version of a stream created by createOriginalReadableStream(). + function testTransferredReadableStream(rs) { + assert_equals(rs.constructor, ReadableStream, + 'rs should be a ReadableStream in this realm'); + assert_true(rs instanceof ReadableStream, + 'instanceof check should pass'); + + // Perform a brand-check on |rs| in the process of calling getReader(). + const reader = ReadableStream.prototype.getReader.call(rs); + + return reader.read().then(({value, done}) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be "a"'); + return reader.read(); + }).then(({done}) => { + assert_true(done, 'done should be true'); + }); + } + + function testMessage(msg) { + assert_array_equals(msg.ports, [], 'there should be no ports in the event'); + return testTransferredReadableStream(msg.data); + } + + function testMessageEvent(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', ev => { + try { + resolve(testMessage(ev)); + } catch (e) { + reject(e); + } + }, {once: true}); + }); + } + + function testMessageEventOrErrorMessage(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', ev => { + if (typeof ev.data === 'string') { + // Assume it's an error message and reject with it. + reject(ev.data); + return; + } + + try { + resolve(testMessage(ev)); + } catch (e) { + reject(e); + } + }, {once: true}); + }); + } + + function checkTestResults(target) { + return new Promise((resolve, reject) => { + target.onmessage = msg => { + // testharness.js sends us objects which we need to ignore. + if (typeof msg.data !== 'string') + return; + + if (msg.data === 'OK') { + resolve(); + } else { + reject(msg.data); + } + }; + }); + } + + // These tests assume that a transferred ReadableStream will behave the same + // regardless of how it was transferred. This enables us to simply transfer the + // stream to ourselves. + function createTransferredReadableStream(underlyingSource) { + const original = new ReadableStream(underlyingSource); + const promise = new Promise((resolve, reject) => { + addEventListener('message', msg => { + const rs = msg.data; + if (rs instanceof ReadableStream) { + resolve(rs); + } else { + reject(new Error(`what is this thing: "${rs}"?`)); + } + }, {once: true}); + }); + postMessage(original, '*', [original]); + return promise; + } + + function recordingTransferredReadableStream(underlyingSource, strategy) { + const original = recordingReadableStream(underlyingSource, strategy); + const promise = new Promise((resolve, reject) => { + addEventListener('message', msg => { + const rs = msg.data; + if (rs instanceof ReadableStream) { + rs.events = original.events; + rs.eventsWithoutPulls = original.eventsWithoutPulls; + rs.controller = original.controller; + resolve(rs); + } else { + reject(new Error(`what is this thing: "${rs}"?`)); + } + }, {once: true}); + }); + postMessage(original, '*', [original]); + return promise; + } + + self.createOriginalReadableStream = createOriginalReadableStream; + self.testMessage = testMessage; + self.testMessageEvent = testMessageEvent; + self.testMessageEventOrErrorMessage = testMessageEventOrErrorMessage; + self.checkTestResults = checkTestResults; + self.createTransferredReadableStream = createTransferredReadableStream; + self.recordingTransferredReadableStream = recordingTransferredReadableStream; + +})();