Repository: servo/servo
Pull request #35831: Refactor common boilerplate out of serialize/transfer implementations
Author: login: jdm
Created: 2025-03-07T05:21:26Z | Merged: 2025-03-15T14:27:26Z

Following up on my original work from #33811, I've got a new set of changes that should make future Serializable/Transferable implementations much easier to write. Unlike #33811, I have not changed anything about how the structured clone implementation is designed—these changes only lift common boilerplate out of the interface-specific logic, and use static types to ensure all known interfaces are handled the same way.

There are two main issues with the current implementation of structured cloning:
* there are many places that need to be updated when adding a new serializable/transferable interface, and the compiler cannot verify that they are all updated
* the interface-specific logic is mixed in with logic that is identical across all interfaces

These changes start by introducing two enums in components/shared/script: Serializable and Transferable. These enums drive all of the logic for cloning/serializing/transfering, ensuring that the compiler will complain if any code requiring interface-specific logic has been missed.

The next important piece of these changes is making the Serializable and Transferable _traits_ from components/script identify which field of StructuredDataReader/Writer should be used for a given type. This ensures that the compiler will complain if those fields are not added when necessary, and allows hoisting the logic for storing/reading serialized/deserialized values out of the interface-specific logic.

The final important piece of these changes is in the trait interfaces for Serializable/Transferable. Now that the common logic is extracted into the common structured cloning implementation, users of these traits only need to write their implementations in terms of their serialized and deserialized types, leading to much simpler code.

[Try run](https://github.com/servo/servo/actions/runs/13714043021/job/38355664511)

---
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] There are tests for these changes


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 700b9a01cb834..1af1a8e7bd43e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1065,7 +1065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
 dependencies = [
  "lazy_static",
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -4294,7 +4294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
 dependencies = [
  "cfg-if",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.6",
 ]
 
 [[package]]
@@ -6471,6 +6471,7 @@ dependencies = [
  "serde",
  "servo_malloc_size_of",
  "servo_url",
+ "strum",
  "strum_macros",
  "style_traits",
  "stylo_atoms",
@@ -8804,7 +8805,7 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
diff --git a/components/script/dom/bindings/serializable.rs b/components/script/dom/bindings/serializable.rs
index edc3b07652c68..b791b0ebc1a88 100644
--- a/components/script/dom/bindings/serializable.rs
+++ b/components/script/dom/bindings/serializable.rs
@@ -5,8 +5,14 @@
 //! Trait representing the concept of [serializable objects]
 //! (<https://html.spec.whatwg.org/multipage/#serializable-objects>).
 
+use std::collections::HashMap;
+use std::num::NonZeroU32;
+
+use base::id::PipelineNamespaceId;
+
 use crate::dom::bindings::reflector::DomObject;
-use crate::dom::bindings::structuredclone::{StructuredDataReader, StructuredDataWriter};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader};
 use crate::dom::globalscope::GlobalScope;
 use crate::script_runtime::CanGc;
 
@@ -18,16 +24,51 @@ pub(crate) struct StorageKey {
     pub(crate) name_space: u32,
 }
 
+impl StorageKey {
+    pub(crate) fn new(name_space: PipelineNamespaceId, index: NonZeroU32) -> StorageKey {
+        let name_space = name_space.0.to_ne_bytes();
+        let index = index.get().to_ne_bytes();
+        StorageKey {
+            index: u32::from_ne_bytes(index),
+            name_space: u32::from_ne_bytes(name_space),
+        }
+    }
+}
+
+pub(crate) trait IntoStorageKey
+where
+    Self: Sized,
+{
+    fn into_storage_key(self) -> StorageKey;
+}
+
 /// Interface for serializable platform objects.
 /// <https://html.spec.whatwg.org/multipage/#serializable>
-pub(crate) trait Serializable: DomObject {
+pub(crate) trait Serializable: DomObject
+where
+    Self: Sized,
+{
+    type Id: Copy + Eq + std::hash::Hash + IntoStorageKey + From<StorageKey>;
+    type Data;
+
     /// <https://html.spec.whatwg.org/multipage/#serialization-steps>
-    fn serialize(&self, sc_writer: &mut StructuredDataWriter) -> Result<StorageKey, ()>;
+    fn serialize(&self) -> Result<(Self::Id, Self::Data), ()>;
     /// <https://html.spec.whatwg.org/multipage/#deserialization-steps>
     fn deserialize(
         owner: &GlobalScope,
-        sc_reader: &mut StructuredDataReader,
-        extra_data: StorageKey,
+        serialized: Self::Data,
         can_gc: CanGc,
-    ) -> Result<(), ()>;
+    ) -> Result<DomRoot<Self>, ()>
+    where
+        Self: Sized;
+
+    /// Returns the field of [StructuredDataReader]/[StructuredDataWriter] that
+    /// should be used to read/store serialized instances of this type.
+    fn serialized_storage(data: StructuredData<'_>) -> &mut Option<HashMap<Self::Id, Self::Data>>;
+
+    /// Returns the field of [StructuredDataReader] that should be used to store
+    /// deserialized instances of this type.
+    fn deserialized_storage(
+        reader: &mut StructuredDataReader,
+    ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>>;
 }
diff --git a/components/script/dom/bindings/structuredclone.rs b/components/script/dom/bindings/structuredclone.rs
index 7716110411fad..077fa1116eb8f 100644
--- a/components/script/dom/bindings/structuredclone.rs
+++ b/components/script/dom/bindings/structuredclone.rs
@@ -5,10 +5,11 @@
 //! This module implements structured cloning, as defined by [HTML](https://html.spec.whatwg.org/multipage/#safe-passing-of-structured-data).
 
 use std::collections::HashMap;
+use std::num::NonZeroU32;
 use std::os::raw;
 use std::ptr;
 
-use base::id::{BlobId, MessagePortId};
+use base::id::{BlobId, MessagePortId, PipelineNamespaceId};
 use js::glue::{
     CopyJSStructuredCloneData, DeleteJSAutoStructuredCloneBuffer, GetLengthOfJSStructuredCloneData,
     NewJSAutoStructuredCloneBuffer, WriteBytesToJSStructuredCloneData,
@@ -22,16 +23,20 @@ use js::jsapi::{
 use js::jsval::UndefinedValue;
 use js::rust::wrappers::{JS_ReadStructuredClone, JS_WriteStructuredClone};
 use js::rust::{CustomAutoRooterGuard, HandleValue, MutableHandleValue};
-use script_traits::StructuredSerializedData;
+use script_bindings::conversions::IDLInterface;
 use script_traits::serializable::BlobImpl;
 use script_traits::transferable::MessagePortImpl;
+use script_traits::{
+    Serializable as SerializableInterface, StructuredSerializedData,
+    Transferrable as TransferrableInterface,
+};
+use strum::IntoEnumIterator;
 
 use crate::dom::bindings::conversions::{ToJSValConvertible, root_from_object};
 use crate::dom::bindings::error::{Error, Fallible};
-use crate::dom::bindings::reflector::DomObject;
 use crate::dom::bindings::root::DomRoot;
-use crate::dom::bindings::serializable::{Serializable, StorageKey};
-use crate::dom::bindings::transferable::Transferable;
+use crate::dom::bindings::serializable::{IntoStorageKey, Serializable, StorageKey};
+use crate::dom::bindings::transferable::{ExtractComponents, IdFromComponents, Transferable};
 use crate::dom::blob::Blob;
 use crate::dom::globalscope::GlobalScope;
 use crate::dom::messageport::MessagePort;
@@ -52,7 +57,36 @@ pub(super) enum StructuredCloneTags {
     Max = 0xFFFFFFFF,
 }
 
-unsafe fn read_blob(
+impl From<SerializableInterface> for StructuredCloneTags {
+    fn from(v: SerializableInterface) -> Self {
+        match v {
+            SerializableInterface::Blob => StructuredCloneTags::DomBlob,
+        }
+    }
+}
+
+impl From<TransferrableInterface> for StructuredCloneTags {
+    fn from(v: TransferrableInterface) -> Self {
+        match v {
+            TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort,
+        }
+    }
+}
+
+fn reader_for_type(
+    val: SerializableInterface,
+) -> unsafe fn(
+    &GlobalScope,
+    *mut JSStructuredCloneReader,
+    &mut StructuredDataReader,
+    CanGc,
+) -> *mut JSObject {
+    match val {
+        SerializableInterface::Blob => read_object::<Blob>,
+    }
+}
+
+unsafe fn read_object<T: Serializable>(
     owner: &GlobalScope,
     r: *mut JSStructuredCloneReader,
     sc_reader: &mut StructuredDataReader,
@@ -66,31 +100,49 @@ unsafe fn read_blob(
         &mut index as *mut u32
     ));
     let storage_key = StorageKey { index, name_space };
-    if <Blob as Serializable>::deserialize(owner, sc_reader, storage_key, can_gc).is_ok() {
-        if let Some(blobs) = &sc_reader.blobs {
-            let blob = blobs
-                .get(&storage_key)
-                .expect("No blob found at storage key.");
-            return blob.reflector().get_jsobject().get();
-        }
+
+    // 1. Re-build the key for the storage location
+    // of the serialized object.
+    let id = T::Id::from(storage_key);
+
+    // 2. Get the transferred object from its storage, using the key.
+    let objects = T::serialized_storage(StructuredData::Reader(sc_reader));
+    let objects_map = objects
+        .as_mut()
+        .expect("The SC holder does not have any relevant objects");
+    let serialized = objects_map
+        .remove(&id)
+        .expect("No object to be deserialized found.");
+    if objects_map.is_empty() {
+        *objects = None;
     }
-    warn!(
-        "Reading structured data for a blob failed in {:?}.",
-        owner.get_url()
-    );
+
+    if let Ok(obj) = T::deserialize(owner, serialized, can_gc) {
+        let destination = T::deserialized_storage(sc_reader).get_or_insert_with(HashMap::new);
+        let reflector = obj.reflector().get_jsobject().get();
+        destination.insert(storage_key, obj);
+        return reflector;
+    }
+    warn!("Reading structured data failed in {:?}.", owner.get_url());
     ptr::null_mut()
 }
 
-unsafe fn write_blob(
+unsafe fn write_object<T: Serializable>(
+    interface: SerializableInterface,
     owner: &GlobalScope,
-    blob: DomRoot<Blob>,
+    object: &T,
     w: *mut JSStructuredCloneWriter,
     sc_writer: &mut StructuredDataWriter,
 ) -> bool {
-    if let Ok(storage_key) = blob.serialize(sc_writer) {
+    if let Ok((new_id, serialized)) = object.serialize() {
+        let objects = T::serialized_storage(StructuredData::Writer(sc_writer))
+            .get_or_insert_with(HashMap::new);
+        objects.insert(new_id, serialized);
+        let storage_key = new_id.into_storage_key();
+
         assert!(JS_WriteUint32Pair(
             w,
-            StructuredCloneTags::DomBlob as u32,
+            StructuredCloneTags::from(interface) as u32,
             0
         ));
         assert!(JS_WriteUint32Pair(
@@ -100,10 +152,7 @@ unsafe fn write_blob(
         ));
         return true;
     }
-    warn!(
-        "Writing structured data for a blob failed in {:?}.",
-        owner.get_url()
-    );
+    warn!("Writing structured data failed in {:?}.", owner.get_url());
     false
 }
 
@@ -123,18 +172,51 @@ unsafe extern "C" fn read_callback(
         tag > StructuredCloneTags::Min as u32,
         "tag should be higher than StructuredCloneTags::Min"
     );
-    if tag == StructuredCloneTags::DomBlob as u32 {
-        let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
-        return read_blob(
-            &GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)),
-            r,
-            &mut *(closure as *mut StructuredDataReader),
-            CanGc::note(),
-        );
+
+    let sc_reader = &mut *(closure as *mut StructuredDataReader);
+    let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
+    let global = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof));
+    for serializable in SerializableInterface::iter() {
+        if tag == StructuredCloneTags::from(serializable) as u32 {
+            let reader = reader_for_type(serializable);
+            return reader(&global, r, sc_reader, CanGc::note());
+        }
     }
+
     ptr::null_mut()
 }
 
+struct InterfaceDoesNotMatch;
+
+unsafe fn try_serialize<T: Serializable + IDLInterface>(
+    val: SerializableInterface,
+    cx: *mut JSContext,
+    obj: RawHandleObject,
+    global: &GlobalScope,
+    w: *mut JSStructuredCloneWriter,
+    writer: &mut StructuredDataWriter,
+) -> Result<bool, InterfaceDoesNotMatch> {
+    if let Ok(obj) = root_from_object::<T>(*obj, cx) {
+        return Ok(write_object(val, global, &*obj, w, writer));
+    }
+    Err(InterfaceDoesNotMatch)
+}
+
+type SerializeOperation = unsafe fn(
+    SerializableInterface,
+    *mut JSContext,
+    RawHandleObject,
+    &GlobalScope,
+    *mut JSStructuredCloneWriter,
+    &mut StructuredDataWriter,
+) -> Result<bool, InterfaceDoesNotMatch>;
+
+fn serialize_for_type(val: SerializableInterface) -> SerializeOperation {
+    match val {
+        SerializableInterface::Blob => try_serialize::<Blob>,
+    }
+}
+
 unsafe extern "C" fn write_callback(
     cx: *mut JSContext,
     w: *mut JSStructuredCloneWriter,
@@ -142,18 +224,73 @@ unsafe extern "C" fn write_callback(
     _same_process_scope_required: *mut bool,
     closure: *mut raw::c_void,
 ) -> bool {
-    if let Ok(blob) = root_from_object::<Blob>(*obj, cx) {
-        let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
-        return write_blob(
-            &GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof)),
-            blob,
-            w,
-            &mut *(closure as *mut StructuredDataWriter),
-        );
+    let sc_writer = &mut *(closure as *mut StructuredDataWriter);
+    let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
+    let global = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof));
+    for serializable in SerializableInterface::iter() {
+        let serializer = serialize_for_type(serializable);
+        if let Ok(result) = serializer(serializable, cx, obj, &global, w, sc_writer) {
+            return result;
+        }
     }
     false
 }
 
+fn receiver_for_type(
+    val: TransferrableInterface,
+) -> fn(&GlobalScope, &mut StructuredDataReader, u64, RawMutableHandleObject) -> Result<(), ()> {
+    match val {
+        TransferrableInterface::MessagePort => receive_object::<MessagePort>,
+    }
+}
+
+fn receive_object<T: Transferable>(
+    owner: &GlobalScope,
+    sc_reader: &mut StructuredDataReader,
+    extra_data: u64,
+    return_object: RawMutableHandleObject,
+) -> Result<(), ()> {
+    // 1. Re-build the key for the storage location
+    // of the transferred object.
+    let big: [u8; 8] = extra_data.to_ne_bytes();
+    let (name_space, index) = big.split_at(4);
+
+    let namespace_id = PipelineNamespaceId(u32::from_ne_bytes(
+        name_space
+            .try_into()
+            .expect("name_space to be a slice of four."),
+    ));
+    let id = <T::Id as IdFromComponents>::from(
+        namespace_id,
+        NonZeroU32::new(u32::from_ne_bytes(
+            index.try_into().expect("index to be a slice of four."),
+        ))
+        .expect("Index to be non-zero"),
+    );
+
+    // 2. Get the transferred object from its storage, using the key.
+    let storage = T::serialized_storage(StructuredData::Reader(sc_reader));
+    let serialized = if let Some(objects) = storage.as_mut() {
+        let object = objects.remove(&id).expect("Transferred port to be stored");
+        if objects.is_empty() {
+            *storage = None;
+        }
+        object
+    } else {
+        panic!(
+            "An interface was transfer-received, yet the SC holder does not have any serialized objects"
+        );
+    };
+
+    if let Ok(received) = T::transfer_receive(owner, id, serialized) {
+        return_object.set(received.reflector().rootable().get());
+        let storage = T::deserialized_storage(sc_reader).get_or_insert_with(Vec::new);
+        storage.push(received);
+        return Ok(());
+    }
+    Err(())
+}
+
 unsafe extern "C" fn read_transfer_callback(
     cx: *mut JSContext,
     _r: *mut JSStructuredCloneReader,
@@ -164,24 +301,73 @@ unsafe extern "C" fn read_transfer_callback(
     closure: *mut raw::c_void,
     return_object: RawMutableHandleObject,
 ) -> bool {
-    if tag == StructuredCloneTags::MessagePort as u32 {
-        let sc_reader = &mut *(closure as *mut StructuredDataReader);
-        let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
-        let owner = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof));
-        if <MessagePort as Transferable>::transfer_receive(
-            &owner,
-            sc_reader,
-            extra_data,
-            return_object,
-        )
-        .is_ok()
-        {
-            return true;
+    let sc_reader = &mut *(closure as *mut StructuredDataReader);
+    let in_realm_proof = AlreadyInRealm::assert_for_cx(SafeJSContext::from_ptr(cx));
+    let owner = GlobalScope::from_context(cx, InRealm::Already(&in_realm_proof));
+    for transferrable in TransferrableInterface::iter() {
+        if tag == StructuredCloneTags::from(transferrable) as u32 {
+            let transfer_receiver = receiver_for_type(transferrable);
+            if transfer_receiver(&owner, sc_reader, extra_data, return_object).is_ok() {
+                return true;
+            }
         }
     }
     false
 }
 
+unsafe fn try_transfer<T: Transferable + IDLInterface>(
+    interface: TransferrableInterface,
+    obj: RawHandleObject,
+    cx: *mut JSContext,
+    sc_writer: &mut StructuredDataWriter,
+    tag: *mut u32,
+    ownership: *mut TransferableOwnership,
+    extra_data: *mut u64,
+) -> Result<(), ()> {
+    if let Ok(object) = root_from_object::<T>(*obj, cx) {
+        *tag = StructuredCloneTags::from(interface) as u32;
+        *ownership = TransferableOwnership::SCTAG_TMO_CUSTOM;
+        if let Ok((id, object)) = object.transfer() {
+            // 2. Store the transferred object at a given key.
+            let objects = T::serialized_storage(StructuredData::Writer(sc_writer))
+                .get_or_insert_with(HashMap::new);
+            objects.insert(id, object);
+
+            let (PipelineNamespaceId(name_space), index) = id.components();
+            let index = index.get();
+
+            let mut big: [u8; 8] = [0; 8];
+            let name_space = name_space.to_ne_bytes();
+            let index = index.to_ne_bytes();
+
+            let (left, right) = big.split_at_mut(4);
+            left.copy_from_slice(&name_space);
+            right.copy_from_slice(&index);
+
+            // 3. Return a u64 representation of the key where the object is stored.
+            *extra_data = u64::from_ne_bytes(big);
+            return Ok(());
+        }
+    }
+    Err(())
+}
+
+type TransferOperation = unsafe fn(
+    TransferrableInterface,
+    RawHandleObject,
+    *mut JSContext,
+    &mut StructuredDataWriter,
+    *mut u32,
+    *mut TransferableOwnership,
+    *mut u64,
+) -> Result<(), ()>;
+
+fn transfer_for_type(val: TransferrableInterface) -> TransferOperation {
+    match val {
+        TransferrableInterface::MessagePort => try_transfer::<MessagePort>,
+    }
+}
+
 /// <https://html.spec.whatwg.org/multipage/#structuredserializewithtransfer>
 unsafe extern "C" fn write_transfer_callback(
     cx: *mut JSContext,
@@ -192,15 +378,14 @@ unsafe extern "C" fn write_transfer_callback(
     _content: *mut *mut raw::c_void,
     extra_data: *mut u64,
 ) -> bool {
-    if let Ok(port) = root_from_object::<MessagePort>(*obj, cx) {
-        *tag = StructuredCloneTags::MessagePort as u32;
-        *ownership = TransferableOwnership::SCTAG_TMO_CUSTOM;
-        let sc_writer = &mut *(closure as *mut StructuredDataWriter);
-        if let Ok(data) = port.transfer(sc_writer) {
-            *extra_data = data;
+    let sc_writer = &mut *(closure as *mut StructuredDataWriter);
+    for transferable in TransferrableInterface::iter() {
+        let try_transfer = transfer_for_type(transferable);
+        if try_transfer(transferable, obj, cx, sc_writer, tag, ownership, extra_data).is_ok() {
             return true;
         }
     }
+
     false
 }
 
@@ -213,14 +398,32 @@ unsafe extern "C" fn free_transfer_callback(
 ) {
 }
 
+unsafe fn can_transfer_for_type(
+    transferable: TransferrableInterface,
+    obj: RawHandleObject,
+    cx: *mut JSContext,
+) -> Result<bool, ()> {
+    unsafe fn can_transfer<T: Transferable + IDLInterface>(
+        obj: RawHandleObject,
+        cx: *mut JSContext,
+    ) -> Result<bool, ()> {
+        root_from_object::<T>(*obj, cx).map(|o| Transferable::can_transfer(&*o))
+    }
+    match transferable {
+        TransferrableInterface::MessagePort => can_transfer::<MessagePort>(obj, cx),
+    }
+}
+
 unsafe extern "C" fn can_transfer_callback(
     cx: *mut JSContext,
     obj: RawHandleObject,
     _same_process_scope_required: *mut bool,
     _closure: *mut raw::c_void,
 ) -> bool {
-    if let Ok(_port) = root_from_object::<MessagePort>(*obj, cx) {
-        return true;
+    for transferable in TransferrableInterface::iter() {
+        if let Ok(can_transfer) = can_transfer_for_type(transferable, obj, cx) {
+            return can_transfer;
+        }
     }
     false
 }
@@ -252,6 +455,11 @@ static STRUCTURED_CLONE_CALLBACKS: JSStructuredCloneCallbacks = JSStructuredClon
     sabCloned: Some(sab_cloned_callback),
 };
 
+pub(crate) enum StructuredData<'a> {
+    Reader(&'a mut StructuredDataReader),
+    Writer(&'a mut StructuredDataWriter),
+}
+
 /// Reader and writer structs for results from, and inputs to, structured-data read/write operations.
 /// <https://html.spec.whatwg.org/multipage/#safe-passing-of-structured-data>
 pub(crate) struct StructuredDataReader {
diff --git a/components/script/dom/bindings/transferable.rs b/components/script/dom/bindings/transferable.rs
index 674d652119b49..7c052e4129492 100644
--- a/components/script/dom/bindings/transferable.rs
+++ b/components/script/dom/bindings/transferable.rs
@@ -5,18 +5,45 @@
 //! Trait representing the concept of [transferable objects]
 //! (<https://html.spec.whatwg.org/multipage/#transferable-objects>).
 
-use js::jsapi::MutableHandleObject;
+use std::collections::HashMap;
+use std::num::NonZeroU32;
+
+use base::id::PipelineNamespaceId;
 
 use crate::dom::bindings::reflector::DomObject;
-use crate::dom::bindings::structuredclone::{StructuredDataReader, StructuredDataWriter};
+use crate::dom::bindings::root::DomRoot;
+use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader};
 use crate::dom::globalscope::GlobalScope;
 
-pub(crate) trait Transferable: DomObject {
-    fn transfer(&self, sc_writer: &mut StructuredDataWriter) -> Result<u64, ()>;
+pub(crate) trait IdFromComponents
+where
+    Self: Sized,
+{
+    fn from(namespace_id: PipelineNamespaceId, index: NonZeroU32) -> Self;
+}
+
+pub(crate) trait ExtractComponents {
+    fn components(&self) -> (PipelineNamespaceId, NonZeroU32);
+}
+
+pub(crate) trait Transferable: DomObject
+where
+    Self: Sized,
+{
+    type Id: Eq + std::hash::Hash + Copy + IdFromComponents + ExtractComponents;
+    type Data;
+
+    fn can_transfer(&self) -> bool {
+        true
+    }
+
+    fn transfer(&self) -> Result<(Self::Id, Self::Data), ()>;
     fn transfer_receive(
         owner: &GlobalScope,
-        sc_reader: &mut StructuredDataReader,
-        extra_data: u64,
-        return_object: MutableHandleObject,
-    ) -> Result<(), ()>;
+        id: Self::Id,
+        serialized: Self::Data,
+    ) -> Result<DomRoot<Self>, ()>;
+
+    fn serialized_storage(data: StructuredData<'_>) -> &mut Option<HashMap<Self::Id, Self::Data>>;
+    fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>>;
 }
diff --git a/components/script/dom/blob.rs b/components/script/dom/blob.rs
index a357fddfaa998..75fa65deff01a 100644
--- a/components/script/dom/blob.rs
+++ b/components/script/dom/blob.rs
@@ -25,9 +25,9 @@ use crate::dom::bindings::codegen::UnionTypes::ArrayBufferOrArrayBufferViewOrBlo
 use crate::dom::bindings::error::{Error, Fallible};
 use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
 use crate::dom::bindings::root::DomRoot;
-use crate::dom::bindings::serializable::{Serializable, StorageKey};
+use crate::dom::bindings::serializable::{IntoStorageKey, Serializable, StorageKey};
 use crate::dom::bindings::str::DOMString;
-use crate::dom::bindings::structuredclone::{StructuredDataReader, StructuredDataWriter};
+use crate::dom::bindings::structuredclone::{StructuredData, StructuredDataReader};
 use crate::dom::globalscope::GlobalScope;
 use crate::dom::promise::Promise;
 use crate::dom::readablestream::ReadableStream;
@@ -94,8 +94,11 @@ impl Blob {
 }
 
 impl Serializable for Blob {
+    type Id = BlobId;
+    type Data = BlobImpl;
+
     /// <https://w3c.github.io/FileAPI/#ref-for-serialization-steps>
-    fn serialize(&self, sc_writer: &mut StructuredDataWriter) -> Result<StorageKey, ()> {
+    fn serialize(&self) -> Result<(BlobId, BlobImpl), ()> {
         let blob_id = self.blob_id;
 
         // 1. Get a clone of the blob impl.
@@ -104,62 +107,52 @@ impl Serializable for Blob {
         // We clone the data, but the clone gets its own Id.
         let new_blob_id = blob_impl.blob_id();
 
-        // 2. Store the object at a given key.
-        let blobs = sc_writer.blobs.get_or_insert_with(HashMap::new);
-        blobs.insert(new_blob_id, blob_impl);
-
-        let PipelineNamespaceId(name_space) = new_blob_id.namespace_id;
-        let BlobIndex(index) = new_blob_id.index;
-        let index = index.get();
-
-        let name_space = name_space.to_ne_bytes();
-        let index = index.to_ne_bytes();
-
-        let storage_key = StorageKey {
-            index: u32::from_ne_bytes(index),
-            name_space: u32::from_ne_bytes(name_space),
-        };
-
-        // 3. Return the storage key.
-        Ok(storage_key)
+        Ok((new_blob_id, blob_impl))
     }
 
     /// <https://w3c.github.io/FileAPI/#ref-for-deserialization-steps>
     fn deserialize(
         owner: &GlobalScope,
-        sc_reader: &mut StructuredDataReader,
-        storage_key: StorageKey,
+        serialized: BlobImpl,
         can_gc: CanGc,
-    ) -> Result<(), ()> {
-        // 1. Re-build the key for the storage location
-        // of the serialized object.
+    ) -> Result<DomRoot<Self>, ()> {
+        let deserialized_blob = Blob::new(owner, serialized, can_gc);
+        Ok(deserialized_blob)
+    }
+
+    fn serialized_storage(
+        reader: StructuredData<'_>,
+    ) -> &mut Option<HashMap<Self::Id, Self::Data>> {
+        match reader {
+            StructuredData::Reader(r) => &mut r.blob_impls,
+            StructuredData::Writer(w) => &mut w.blobs,
+        }
+    }
+
+    fn deserialized_storage(
+        data: &mut StructuredDataReader,
+    ) -> &mut Option<HashMap<StorageKey, DomRoot<Self>>> {
+        &mut data.blobs
+    }
+}
+
+impl From<StorageKey> for BlobId {
+    fn from(storage_key: StorageKey) -> BlobId {
         let namespace_id = PipelineNamespaceId(storage_key.name_space);
         let index =
             BlobIndex(NonZeroU32::new(storage_key.index).expect("Deserialized blob index is zero"));
 
-        let id = BlobId {
+        BlobId {
             namespace_id,
             index,
-        };
-
-        // 2. Get the transferred object from its storage, using the key.
-        let blob_impls_map = sc_reader
-            .blob_impls
-            .as_mut()
-            .expect("The SC holder does not have any blob impls");
-        let blob_impl = blob_impls_map
-            .remove(&id)
-            .expect("No blob to be deserialized found.");
-        if blob_impls_map.is_empty() {
-            sc_reader.blob_impls = None;
         }
+    }
+}
 
-        let deserialized_blob = Blob::new(owner, blob_impl, can_gc);
-
-        let blobs = sc_reader.blobs.get_or_insert_with(HashMap::new);
-        blobs.insert(storage_key, deserialized_blob);
-
-        Ok(())
+impl IntoStorageKey for BlobId {
+    fn into_storage_key(self) -> StorageKey {
+        let BlobIndex(index) = self.index;
+        StorageKey::new(self.namespace_id, index)
     }
 }
 
diff --git a/components/script/dom/messageport.rs b/components/script/dom/messageport.rs
index e8a3b3ac695b9..f10ba10383e3f 100644
--- a/components/script/dom/messageport.rs
+++ b/components/script/dom/messageport.rs
@@ -4,15 +4,15 @@
 
 use std::cell::{Cell, RefCell};
 use std::collections::HashMap;
-use std::convert::TryInto;
 use std::num::NonZeroU32;
 use std::rc::Rc;
 
 use base::id::{MessagePortId, MessagePortIndex, PipelineNamespaceId};
 use dom_struct::dom_struct;
-use js::jsapi::{Heap, JSObject, MutableHandleObject};
+use js::jsapi::{Heap, JSObject};
 use js::rust::{CustomAutoRooter, CustomAutoRooterGuard, HandleValue};
 use script_traits::PortMessageTask;
+use script_traits::transferable::MessagePortImpl;
 
 use crate::dom::bindings::codegen::Bindings::EventHandlerBinding::EventHandlerNonNull;
 use crate::dom::bindings::codegen::Bindings::MessagePortBinding::{
@@ -21,11 +21,11 @@ use crate::dom::bindings::codegen::Bindings::MessagePortBinding::{
 use crate::dom::bindings::conversions::root_from_object;
 use crate::dom::bindings::error::{Error, ErrorResult};
 use crate::dom::bindings::inheritance::Castable;
-use crate::dom::bindings::reflector::{DomGlobal, DomObject, reflect_dom_object};
+use crate::dom::bindings::reflector::{DomGlobal, reflect_dom_object};
 use crate::dom::bindings::root::DomRoot;
-use crate::dom::bindings::structuredclone::{self, StructuredDataReader, StructuredDataWriter};
+use crate::dom::bindings::structuredclone::{self, StructuredData, StructuredDataReader};
 use crate::dom::bindings::trace::RootedTraceableBox;
-use crate::dom::bindings::transferable::Transferable;
+use crate::dom::bindings::transferable::{ExtractComponents, IdFromComponents, Transferable};
 use crate::dom::eventtarget::EventTarget;
 use crate::dom::globalscope::GlobalScope;
 use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
@@ -160,8 +160,11 @@ impl MessagePort {
 }
 
 impl Transferable for MessagePort {
+    type Id = MessagePortId;
+    type Data = MessagePortImpl;
+
     /// <https://html.spec.whatwg.org/multipage/#message-ports:transfer-steps>
-    fn transfer(&self, sc_writer: &mut StructuredDataWriter) -> Result<u64, ()> {
+    fn transfer(&self) -> Result<(MessagePortId, MessagePortImpl), ()> {
         if self.detached.get() {
             return Err(());
         }
@@ -172,91 +175,45 @@ impl Transferable for MessagePort {
         // 1. Run local transfer logic, and return the object to be transferred.
         let transferred_port = self.global().mark_port_as_transferred(id);
 
-        // 2. Store the transferred object at a given key.
-        if let Some(ports) = sc_writer.ports.as_mut() {
-            ports.insert(*id, transferred_port);
-        } else {
-            let mut ports = HashMap::new();
-            ports.insert(*id, transferred_port);
-            sc_writer.ports = Some(ports);
-        }
-
-        let PipelineNamespaceId(name_space) = (id).namespace_id;
-        let MessagePortIndex(index) = (id).index;
-        let index = index.get();
-
-        let mut big: [u8; 8] = [0; 8];
-        let name_space = name_space.to_ne_bytes();
-        let index = index.to_ne_bytes();
-
-        let (left, right) = big.split_at_mut(4);
-        left.copy_from_slice(&name_space);
-        right.copy_from_slice(&index);
-
-        // 3. Return a u64 representation of the key where the object is stored.
-        Ok(u64::from_ne_bytes(big))
+        Ok((*id, transferred_port))
     }
 
     /// <https://html.spec.whatwg.org/multipage/#message-ports:transfer-receiving-steps>
     fn transfer_receive(
         owner: &GlobalScope,
-        sc_reader: &mut StructuredDataReader,
-        extra_data: u64,
-        return_object: MutableHandleObject,
-    ) -> Result<(), ()> {
-        // 1. Re-build the key for the storage location
-        // of the transferred object.
-        let big: [u8; 8] = extra_data.to_ne_bytes();
-        let (name_space, index) = big.split_at(4);
-
-        let namespace_id = PipelineNamespaceId(u32::from_ne_bytes(
-            name_space
-                .try_into()
-                .expect("name_space to be a slice of four."),
-        ));
-        let index = MessagePortIndex(
-            NonZeroU32::new(u32::from_ne_bytes(
-                index.try_into().expect("index to be a slice of four."),
-            ))
-            .expect("Index to be non-zero"),
-        );
-
-        let id = MessagePortId {
-            namespace_id,
-            index,
-        };
-
-        // 2. Get the transferred object from its storage, using the key.
-        // Assign the transfer-received port-impl, and total number of transferred ports.
-        let (ports_len, port_impl) = if let Some(ports) = sc_reader.port_impls.as_mut() {
-            let ports_len = ports.len();
-            let port_impl = ports.remove(&id).expect("Transferred port to be stored");
-            if ports.is_empty() {
-                sc_reader.port_impls = None;
-            }
-            (ports_len, port_impl)
-        } else {
-            panic!(
-                "A messageport was transfer-received, yet the SC holder does not have any port impls"
-            );
-        };
-
+        id: MessagePortId,
+        port_impl: MessagePortImpl,
+    ) -> Result<DomRoot<Self>, ()> {
         let transferred_port =
             MessagePort::new_transferred(owner, id, port_impl.entangled_port_id(), CanGc::note());
         owner.track_message_port(&transferred_port, Some(port_impl));
+        Ok(transferred_port)
+    }
+
+    fn serialized_storage(data: StructuredData<'_>) -> &mut Option<HashMap<Self::Id, Self::Data>> {
+        match data {
+            StructuredData::Reader(r) => &mut r.port_impls,
+            StructuredData::Writer(w) => &mut w.ports,
+        }
+    }
 
-        return_object.set(transferred_port.reflector().rootable().get());
+    fn deserialized_storage(reader: &mut StructuredDataReader) -> &mut Option<Vec<DomRoot<Self>>> {
+        &mut reader.message_ports
+    }
+}
 
-        // Store the DOM port where it will be passed along to script in the message-event.
-        if let Some(ports) = sc_reader.message_ports.as_mut() {
-            ports.push(transferred_port);
-        } else {
-            let mut ports = Vec::with_capacity(ports_len);
-            ports.push(transferred_port);
-            sc_reader.message_ports = Some(ports);
+impl IdFromComponents for MessagePortId {
+    fn from(namespace_id: PipelineNamespaceId, index: NonZeroU32) -> MessagePortId {
+        MessagePortId {
+            namespace_id,
+            index: MessagePortIndex(index),
         }
+    }
+}
 
-        Ok(())
+impl ExtractComponents for MessagePortId {
+    fn components(&self) -> (PipelineNamespaceId, NonZeroU32) {
+        (self.namespace_id, self.index.0)
     }
 }
 
diff --git a/components/shared/script/Cargo.toml b/components/shared/script/Cargo.toml
index 3efe597d43f36..18bf997c34ad1 100644
--- a/components/shared/script/Cargo.toml
+++ b/components/shared/script/Cargo.toml
@@ -42,6 +42,7 @@ serde = { workspace = true }
 strum_macros = { workspace = true }
 stylo_atoms = { workspace = true }
 servo_url = { path = "../../url" }
+strum = { workspace = true, features = ["derive"] }
 style_traits = { workspace = true }
 uuid = { workspace = true }
 webdriver = { workspace = true }
diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs
index 5e0fe996e7a2a..9df9624ebeed5 100644
--- a/components/shared/script/lib.rs
+++ b/components/shared/script/lib.rs
@@ -50,6 +50,7 @@ use pixels::PixelFormat;
 use profile_traits::{mem, time as profile_time};
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use servo_url::{ImmutableOrigin, ServoUrl};
+use strum::{EnumIter, IntoEnumIterator};
 use strum_macros::IntoStaticStr;
 use style_traits::{CSSPixel, SpeculativePainter};
 use stylo_atoms::Atom;
@@ -67,7 +68,7 @@ pub use crate::script_msg::{
     LogEntry, SWManagerMsg, SWManagerSenders, ScopeThings, ScriptMsg, ServiceWorkerMsg,
     TouchEventResult,
 };
-use crate::serializable::{BlobData, BlobImpl};
+use crate::serializable::BlobImpl;
 use crate::transferable::MessagePortImpl;
 
 /// The address of a node. Layout sends these back. They must be validated via
@@ -741,47 +742,96 @@ pub struct StructuredSerializedData {
     pub ports: Option<HashMap<MessagePortId, MessagePortImpl>>,
 }
 
-impl StructuredSerializedData {
-    /// Clone the serialized data for use with broadcast-channels.
-    pub fn clone_for_broadcast(&self) -> StructuredSerializedData {
-        let serialized = self.serialized.clone();
+pub(crate) trait BroadcastClone
+where
+    Self: Sized,
+{
+    /// The ID type that uniquely identify each value.
+    type Id: Eq + std::hash::Hash + Copy;
+    /// Clone this value so that it can be reused with a broadcast channel.
+    /// Only return None if cloning is impossible.
+    fn clone_for_broadcast(&self) -> Option<Self>;
+    /// The field from which to clone values.
+    fn source(data: &StructuredSerializedData) -> &Option<HashMap<Self::Id, Self>>;
+    /// The field into which to place cloned values.
+    fn destination(data: &mut StructuredSerializedData) -> &mut Option<HashMap<Self::Id, Self>>;
+}
+
+/// All the DOM interfaces that can be serialized.
+#[derive(Clone, Copy, Debug, EnumIter)]
+pub enum Serializable {
+    /// The `Blob` interface.
+    Blob,
+}
+
+impl Serializable {
+    fn clone_values(&self) -> fn(&StructuredSerializedData, &mut StructuredSerializedData) {
+        match self {
+            Serializable::Blob => StructuredSerializedData::clone_all_of_type::<BlobImpl>,
+        }
+    }
+}
 
-        let blobs = if let Some(blobs) = self.blobs.as_ref() {
-            let mut blob_clones = HashMap::with_capacity(blobs.len());
+/// All the DOM interfaces that can be transferred.
+#[derive(Clone, Copy, Debug, EnumIter)]
+pub enum Transferrable {
+    /// The `MessagePort` interface.
+    MessagePort,
+}
 
-            for (original_id, blob) in blobs.iter() {
-                let type_string = blob.type_string();
+impl StructuredSerializedData {
+    fn is_empty(&self, val: Transferrable) -> bool {
+        fn is_field_empty<K, V>(field: &Option<HashMap<K, V>>) -> bool {
+            field.as_ref().is_some_and(|h| h.is_empty())
+        }
+        match val {
+            Transferrable::MessagePort => is_field_empty(&self.ports),
+        }
+    }
 
-                if let BlobData::Memory(bytes) = blob.blob_data() {
-                    let blob_clone = BlobImpl::new_from_bytes(bytes.clone(), type_string);
+    /// Clone all values of the same type stored in this StructuredSerializedData
+    /// into another instance.
+    fn clone_all_of_type<T: BroadcastClone>(&self, cloned: &mut StructuredSerializedData) {
+        let existing = T::source(self);
+        let Some(existing) = existing else { return };
+        let mut clones = HashMap::with_capacity(existing.len());
 
-                    // Note: we insert the blob at the original id,
-                    // otherwise this will not match the storage key as serialized by SM in `serialized`.
-                    // The clone has it's own new Id however.
-                    blob_clones.insert(*original_id, blob_clone);
-                } else {
-                    // Not panicking only because this is called from the constellation.
-                    warn!("Serialized blob not in memory format(should never happen).");
-                }
+        for (original_id, obj) in existing.iter() {
+            if let Some(clone) = obj.clone_for_broadcast() {
+                clones.insert(*original_id, clone);
             }
-            Some(blob_clones)
-        } else {
-            None
-        };
+        }
+
+        *T::destination(cloned) = Some(clones);
+    }
 
-        if self.ports.is_some() {
-            // Not panicking only because this is called from the constellation.
-            warn!(
-                "Attempt to broadcast structured serialized data including ports(should never happen)."
-            );
+    /// Clone the serialized data for use with broadcast-channels.
+    pub fn clone_for_broadcast(&self) -> StructuredSerializedData {
+        for transferrable in Transferrable::iter() {
+            if !self.is_empty(transferrable) {
+                // Not panicking only because this is called from the constellation.
+                warn!(
+                    "Attempt to broadcast structured serialized data including {:?} (should never happen).",
+                    transferrable,
+                );
+            }
         }
 
-        StructuredSerializedData {
+        let serialized = self.serialized.clone();
+
+        let mut cloned = StructuredSerializedData {
             serialized,
-            blobs,
+            blobs: None,
             // Ports cannot be broadcast.
             ports: None,
+        };
+
+        for serializable in Serializable::iter() {
+            let clone_impl = serializable.clone_values();
+            clone_impl(self, &mut cloned);
         }
+
+        cloned
     }
 }
 
diff --git a/components/shared/script/serializable.rs b/components/shared/script/serializable.rs
index d168736f961c2..8e9f9f5b3daab 100644
--- a/components/shared/script/serializable.rs
+++ b/components/shared/script/serializable.rs
@@ -60,6 +60,39 @@ impl FileBlob {
     }
 }
 
+impl crate::BroadcastClone for BlobImpl {
+    type Id = BlobId;
+
+    fn source(
+        data: &crate::StructuredSerializedData,
+    ) -> &Option<std::collections::HashMap<Self::Id, Self>> {
+        &data.blobs
+    }
+
+    fn destination(
+        data: &mut crate::StructuredSerializedData,
+    ) -> &mut Option<std::collections::HashMap<Self::Id, Self>> {
+        &mut data.blobs
+    }
+
+    fn clone_for_broadcast(&self) -> Option<Self> {
+        let type_string = self.type_string();
+
+        if let BlobData::Memory(bytes) = self.blob_data() {
+            let blob_clone = BlobImpl::new_from_bytes(bytes.clone(), type_string);
+
+            // Note: we insert the blob at the original id,
+            // otherwise this will not match the storage key as serialized by SM in `serialized`.
+            // The clone has it's own new Id however.
+            return Some(blob_clone);
+        } else {
+            // Not panicking only because this is called from the constellation.
+            log::warn!("Serialized blob not in memory format(should never happen).");
+        }
+        None
+    }
+}
+
 /// The data backing a DOM Blob.
 #[derive(Debug, Deserialize, MallocSizeOf, Serialize)]
 pub struct BlobImpl {


Reviews:
user: login: gterzian
body: LGTM, nice work!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T09:31:17Z
user: login: jdm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T01:10:57Z

Review comments:
diff_hunk: @@ -52,7 +57,36 @@ pub(super) enum StructuredCloneTags {
     Max = 0xFFFFFFFF,
 }
 
-unsafe fn read_blob(
+impl From<SerializableInterface> for StructuredCloneTags {
+    fn from(v: SerializableInterface) -> Self {
+        match v {
+            SerializableInterface::Blob => StructuredCloneTags::DomBlob,
+        }
+    }
+}
+
+impl From<TransferrableInterface> for StructuredCloneTags {
+    fn from(v: TransferrableInterface) -> Self {
+        match v {
+            TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort,
+        }
+    }
+}
+
+fn reader_for_type(
+    val: SerializableInterface,
path: components/script/dom/bindings/structuredclone.rs
user: login: gterzian
body: Can you not pass `StructuredCloneTags` here, it seems this would enable removing the loop with the `StructuredCloneTags::from(serializable)` check, but it's hard to tell without trying. 
created_at: 2025-03-10T09:30:40Z
author_association: MEMBER
diff_hunk: @@ -52,7 +57,36 @@ pub(super) enum StructuredCloneTags {
     Max = 0xFFFFFFFF,
 }
 
-unsafe fn read_blob(
+impl From<SerializableInterface> for StructuredCloneTags {
+    fn from(v: SerializableInterface) -> Self {
+        match v {
+            SerializableInterface::Blob => StructuredCloneTags::DomBlob,
+        }
+    }
+}
+
+impl From<TransferrableInterface> for StructuredCloneTags {
+    fn from(v: TransferrableInterface) -> Self {
+        match v {
+            TransferrableInterface::MessagePort => StructuredCloneTags::MessagePort,
+        }
+    }
+}
+
+fn reader_for_type(
+    val: SerializableInterface,
path: components/script/dom/bindings/structuredclone.rs
user: login: jdm
body: I don't believe this is possible in this design. StructuredCloneTags includes all serializable and transferable types plus additional marker values, which is why I wanted a separate enum with only the relevant interfaces. I _think_ what you're asking for is "given this structured clone tag value, give me the enum value corresponding to the type we need to deserialize", but that requires a match expression which is much harder to make impossible to get wrong.
created_at: 2025-03-13T01:10:57Z
author_association: MEMBER

PR comments:
user: login: gterzian
created_at: 2025-03-12T09:02:39Z
body: One thing I will be working on soon is the transfer of streams: https://streams.spec.whatwg.org/#rs-transfer

And this actually uses a message port internally. 

I haven't figured out too much into how to do it, but I think I will leverage what we already do with ports and fit that in the transfer steps of the stream. 

So that will be an interesting test of the new setup. 

It might be worth it having a look at the spec just to see if the more generic setup doesn't actually constrict us to do this kind nested transfer(I'm not good enough with generics to figure this out just by reading the code). 
author_association: MEMBER
user: login: jdm
created_at: 2025-03-13T00:36:28Z
body: > One thing I will be working on soon is the transfer of streams: https://streams.spec.whatwg.org/#rs-transfer
> 
> And this actually uses a message port internally.
> 
> I haven't figured out too much into how to do it, but I think I will leverage what we already do with ports and fit that in the transfer steps of the stream.
> 
> So that will be an interesting test of the new setup.
> 
> It might be worth it having a look at the spec just to see if the more generic setup doesn't actually constrict us to do this kind nested transfer(I'm not good enough with generics to figure this out just by reading the code).

Yikes, this is such an unexpected way of transferring streams. I'll definitely need to look through this closely.
author_association: MEMBER
user: login: jdm
created_at: 2025-03-13T00:38:51Z
body: Note to self: can_transfer_callback needs to be updated as well before this merges.
author_association: MEMBER
user: login: jdm
created_at: 2025-03-13T00:48:00Z
body: > One thing I will be working on soon is the transfer of streams: https://streams.spec.whatwg.org/#rs-transfer
> 
> And this actually uses a message port internally.
> 
> I haven't figured out too much into how to do it, but I think I will leverage what we already do with ports and fit that in the transfer steps of the stream.
> 
> So that will be an interesting test of the new setup.
> 
> It might be worth it having a look at the spec just to see if the more generic setup doesn't actually constrict us to do this kind nested transfer(I'm not good enough with generics to figure this out just by reading the code).

I _think_ the new setup works if we specify that ReadableStream's Transferable::Id is MessagePortId and Transferable::Data is MessagePortImpl. That allows the transfer steps for the stream to transfer a message port, and then the transfer receive steps will return a new ReadableStream given the transferred message port.
author_association: MEMBER