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;
+
+})();