Skip to main content

hydro_lang/live_collections/stream/
networking.rs

1//! Networking APIs for [`Stream`].
2
3use std::marker::PhantomData;
4
5use serde::Serialize;
6use serde::de::DeserializeOwned;
7use stageleft::{q, quote_type};
8use syn::parse_quote;
9
10use super::{ExactlyOnce, MinOrder, Ordering, Stream, TotalOrder};
11use crate::compile::builder::ExternalPortId;
12use crate::compile::ir::{
13    DebugInstantiate, HydroIrOpMetadata, HydroNode, HydroRoot, NetworkRecv, NetworkSend,
14};
15use crate::live_collections::boundedness::{Boundedness, Unbounded};
16use crate::live_collections::keyed_singleton::{KeyedSingleton, MonotonicKeys};
17use crate::live_collections::keyed_stream::KeyedStream;
18use crate::live_collections::sliced::sliced;
19use crate::live_collections::stream::Retries;
20#[cfg(feature = "sim")]
21use crate::location::LocationKey;
22use crate::location::cluster::{ClusterIds, Consistency, NoConsistency};
23#[cfg(stageleft_runtime)]
24use crate::location::dynamic::DynLocation;
25use crate::location::external_process::ExternalBincodeStream;
26use crate::location::{Cluster, External, Location, MemberId, MembershipEvent, Process};
27use crate::networking::{NetworkFor, TCP};
28use crate::nondet::{NonDet, nondet};
29use crate::properties::manual_proof;
30#[cfg(feature = "sim")]
31use crate::sim::SimReceiver;
32use crate::staging_util::get_this_crate;
33
34// same as the one in `hydro_std`, but internal use only
35fn track_membership<'a, C, L: Location<'a>>(
36    membership: KeyedStream<MemberId<C>, MembershipEvent, L, Unbounded>,
37) -> KeyedSingleton<MemberId<C>, bool, L, MonotonicKeys> {
38    membership.fold(
39        q!(|| false),
40        q!(|present, event| {
41            match event {
42                MembershipEvent::Joined => *present = true,
43                MembershipEvent::Left => *present = false,
44            }
45        }),
46    )
47}
48
49fn serialize_bincode_with_type(is_demux: bool, t_type: &syn::Type) -> syn::Expr {
50    let root = get_this_crate();
51
52    if is_demux {
53        parse_quote! {
54            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<(#root::__staged::location::MemberId<_>, #t_type), _>(
55                |(id, data)| {
56                    (id.into_tagless(), #root::runtime_support::bincode::serialize(&data).unwrap().into())
57                }
58            )
59        }
60    } else {
61        parse_quote! {
62            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#t_type, _>(
63                |data| {
64                    #root::runtime_support::bincode::serialize(&data).unwrap().into()
65                }
66            )
67        }
68    }
69}
70
71pub(crate) fn serialize_bincode<T: Serialize>(is_demux: bool) -> syn::Expr {
72    serialize_bincode_with_type(is_demux, &quote_type::<T>())
73}
74
75fn deserialize_bincode_with_type(tagged: Option<&syn::Type>, t_type: &syn::Type) -> syn::Expr {
76    let root = get_this_crate();
77    if let Some(c_type) = tagged {
78        parse_quote! {
79            |res| {
80                let (id, b) = res.unwrap();
81                (#root::__staged::location::MemberId::<#c_type>::from_tagless(id as #root::__staged::location::TaglessMemberId), #root::runtime_support::bincode::deserialize::<#t_type>(&b).unwrap())
82            }
83        }
84    } else {
85        parse_quote! {
86            |res| {
87                #root::runtime_support::bincode::deserialize::<#t_type>(&res.unwrap()).unwrap()
88            }
89        }
90    }
91}
92
93pub(crate) fn deserialize_bincode<T: DeserializeOwned>(tagged: Option<&syn::Type>) -> syn::Expr {
94    deserialize_bincode_with_type(tagged, &quote_type::<T>())
95}
96
97impl<'a, T, L, B: Boundedness, O: Ordering, R: Retries> Stream<T, Process<'a, L>, B, O, R> {
98    #[deprecated = "use Stream::send(..., TCP.fail_stop().bincode()) instead"]
99    /// "Moves" elements of this stream to a new distributed location by sending them over the network,
100    /// using [`bincode`] to serialize/deserialize messages.
101    ///
102    /// The returned stream captures the elements received at the destination, where values will
103    /// asynchronously arrive over the network. Sending from a [`Process`] to another [`Process`]
104    /// preserves ordering and retries guarantees by using a single TCP channel to send the values. The
105    /// recipient is guaranteed to receive a _prefix_ or the sent messages; if the TCP connection is
106    /// dropped no further messages will be sent.
107    ///
108    /// # Example
109    /// ```rust
110    /// # #[cfg(feature = "deploy")] {
111    /// # use hydro_lang::prelude::*;
112    /// # use futures::StreamExt;
113    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p_out| {
114    /// let p1 = flow.process::<()>();
115    /// let numbers: Stream<_, Process<_>, Bounded> = p1.source_iter(q!(vec![1, 2, 3]));
116    /// let p2 = flow.process::<()>();
117    /// let on_p2: Stream<_, Process<_>, Unbounded> = numbers.send_bincode(&p2);
118    /// // 1, 2, 3
119    /// # on_p2.send_bincode(&p_out)
120    /// # }, |mut stream| async move {
121    /// # for w in 1..=3 {
122    /// #     assert_eq!(stream.next().await, Some(w));
123    /// # }
124    /// # }));
125    /// # }
126    /// ```
127    pub fn send_bincode<L2>(
128        self,
129        other: &Process<'a, L2>,
130    ) -> Stream<T, Process<'a, L2>, Unbounded, O, R>
131    where
132        T: Serialize + DeserializeOwned,
133    {
134        self.send(other, TCP.fail_stop().bincode())
135    }
136
137    /// "Moves" elements of this stream to a new distributed location by sending them over the network,
138    /// using the configuration in `via` to set up the message transport.
139    ///
140    /// The returned stream captures the elements received at the destination, where values will
141    /// asynchronously arrive over the network. Sending from a [`Process`] to another [`Process`]
142    /// preserves ordering and retries guarantees when using a single TCP channel to send the values.
143    /// The recipient is guaranteed to receive a _prefix_ or the sent messages; if the connection is
144    /// dropped no further messages will be sent.
145    ///
146    /// # Example
147    /// ```rust
148    /// # #[cfg(feature = "deploy")] {
149    /// # use hydro_lang::prelude::*;
150    /// # use futures::StreamExt;
151    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p_out| {
152    /// let p1 = flow.process::<()>();
153    /// let numbers: Stream<_, Process<_>, Bounded> = p1.source_iter(q!(vec![1, 2, 3]));
154    /// let p2 = flow.process::<()>();
155    /// let on_p2: Stream<_, Process<_>, Unbounded> = numbers.send(&p2, TCP.fail_stop().bincode());
156    /// // 1, 2, 3
157    /// # on_p2.send(&p_out, TCP.fail_stop().bincode())
158    /// # }, |mut stream| async move {
159    /// # for w in 1..=3 {
160    /// #     assert_eq!(stream.next().await, Some(w));
161    /// # }
162    /// # }));
163    /// # }
164    /// ```
165    pub fn send<L2, N: NetworkFor<T>>(
166        self,
167        to: &Process<'a, L2>,
168        via: N,
169    ) -> Stream<T, Process<'a, L2>, Unbounded, <O as MinOrder<N::OrderingGuarantee>>::Min, R>
170    where
171        O: MinOrder<N::OrderingGuarantee>,
172    {
173        let name = via.name();
174        if to.multiversioned() && name.is_none() {
175            panic!(
176                "Cannot send to a multiversioned location without a channel name. Please provide a name for the network."
177            );
178        }
179
180        let (serialize, deserialize) = if N::is_embedded() {
181            (
182                NetworkSend::Embedded {
183                    tag: None,
184                    element_type: quote_type::<T>().into(),
185                },
186                NetworkRecv::Embedded {
187                    tag: None,
188                    element_type: quote_type::<T>().into(),
189                },
190            )
191        } else {
192            (
193                NetworkSend::Custom {
194                    serialize_fn: Some(N::serialize_thunk(false).into()),
195                },
196                NetworkRecv::Custom {
197                    deserialize_fn: Some(N::deserialize_thunk(None).into()),
198                },
199            )
200        };
201
202        Stream::new(
203            to.clone(),
204            HydroNode::Network {
205                name: name.map(ToOwned::to_owned),
206                networking_info: N::networking_info(),
207                serialize,
208                deserialize,
209                instantiate_fn: DebugInstantiate::Building,
210                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
211                metadata: to.new_node_metadata(Stream::<
212                    T,
213                    Process<'a, L2>,
214                    Unbounded,
215                    <O as MinOrder<N::OrderingGuarantee>>::Min,
216                    R,
217                >::collection_kind()),
218            },
219        )
220    }
221
222    #[deprecated = "use Stream::broadcast(..., TCP.fail_stop().bincode()) instead"]
223    /// Broadcasts elements of this stream to all members of a cluster by sending them over the network,
224    /// using [`bincode`] to serialize/deserialize messages.
225    ///
226    /// Each element in the stream will be sent to **every** member of the cluster based on the latest
227    /// membership information. This is a common pattern in distributed systems for broadcasting data to
228    /// all nodes in a cluster. Unlike [`Stream::demux_bincode`], which requires `(MemberId, T)` tuples to
229    /// target specific members, `broadcast_bincode` takes a stream of **only data elements** and sends
230    /// each element to all cluster members.
231    ///
232    /// # Non-Determinism
233    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
234    /// to the current cluster members _at that point in time_. Depending on when we are notified of
235    /// membership changes, we will broadcast each element to different members.
236    ///
237    /// # Example
238    /// ```rust
239    /// # #[cfg(feature = "deploy")] {
240    /// # use hydro_lang::prelude::*;
241    /// # use futures::StreamExt;
242    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
243    /// let p1 = flow.process::<()>();
244    /// let workers: Cluster<()> = flow.cluster::<()>();
245    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
246    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.broadcast_bincode(&workers, nondet!(/** assuming stable membership */));
247    /// # on_worker.send_bincode(&p2).entries()
248    /// // if there are 4 members in the cluster, each receives one element
249    /// // - MemberId::<()>(0): [123]
250    /// // - MemberId::<()>(1): [123]
251    /// // - MemberId::<()>(2): [123]
252    /// // - MemberId::<()>(3): [123]
253    /// # }, |mut stream| async move {
254    /// # let mut results = Vec::new();
255    /// # for w in 0..4 {
256    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
257    /// # }
258    /// # results.sort();
259    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
260    /// # }));
261    /// # }
262    /// ```
263    pub fn broadcast_bincode<L2: 'a>(
264        self,
265        other: &Cluster<'a, L2>,
266        nondet_membership: NonDet,
267    ) -> Stream<T, Cluster<'a, L2>, Unbounded, O, R>
268    where
269        T: Clone + Serialize + DeserializeOwned,
270    {
271        self.broadcast(other, TCP.fail_stop().bincode(), nondet_membership)
272    }
273
274    /// Broadcasts elements of this stream to all members of a cluster by sending them over the network,
275    /// using the configuration in `via` to set up the message transport.
276    ///
277    /// Each element in the stream will be sent to **every** member of the cluster based on the latest
278    /// membership information. This is a common pattern in distributed systems for broadcasting data to
279    /// all nodes in a cluster. Unlike [`Stream::demux`], which requires `(MemberId, T)` tuples to
280    /// target specific members, `broadcast` takes a stream of **only data elements** and sends
281    /// each element to all cluster members.
282    ///
283    /// # Non-Determinism
284    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
285    /// to the current cluster members _at that point in time_. Depending on when we are notified of
286    /// membership changes, we will broadcast each element to different members.
287    ///
288    /// # Example
289    /// ```rust
290    /// # #[cfg(feature = "deploy")] {
291    /// # use hydro_lang::prelude::*;
292    /// # use futures::StreamExt;
293    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
294    /// let p1 = flow.process::<()>();
295    /// let workers: Cluster<()> = flow.cluster::<()>();
296    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
297    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.broadcast(&workers, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
298    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
299    /// // if there are 4 members in the cluster, each receives one element
300    /// // - MemberId::<()>(0): [123]
301    /// // - MemberId::<()>(1): [123]
302    /// // - MemberId::<()>(2): [123]
303    /// // - MemberId::<()>(3): [123]
304    /// # }, |mut stream| async move {
305    /// # let mut results = Vec::new();
306    /// # for w in 0..4 {
307    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
308    /// # }
309    /// # results.sort();
310    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
311    /// # }));
312    /// # }
313    /// ```
314    pub fn broadcast<L2: 'a, N: NetworkFor<T>>(
315        self,
316        to: &Cluster<'a, L2>,
317        via: N,
318        nondet_membership: NonDet,
319    ) -> Stream<T, Cluster<'a, L2>, Unbounded, <O as MinOrder<N::OrderingGuarantee>>::Min, R>
320    where
321        T: Clone,
322        O: MinOrder<N::OrderingGuarantee>,
323    {
324        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
325        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
326        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
327        // can script the membership snapshot and the element batching independently.
328        let ids = track_membership(self.location.source_cluster_membership_stream(
329            to,
330            nondet!(/** dropped prefixes don't affect broadcast */),
331        ));
332        sliced! {
333            let members_snapshot = use::snapshot(ids, nondet!(
334                /// membership timing is captured by the caller's guard
335                nondet_membership
336            ));
337            let elements = use::batch(self, nondet!(
338                /// batching timing is captured by the caller's guard
339                nondet_membership
340            ));
341
342            let current_members = members_snapshot.filter(q!(|b| *b));
343            elements.repeat_with_keys(current_members)
344        }
345        .demux(to, via)
346    }
347
348    /// Broadcasts elements of this stream to all members of a cluster,
349    /// assuming membership is closed (fixed at deploy time).
350    ///
351    /// Unlike [`Stream::broadcast`], this does not require a [`NonDet`] guard.
352    /// The membership set is obtained from deploy metadata via
353    /// [`ClusterIds`], producing a
354    /// `Bounded` stream. The cross-product of data × members is fully
355    /// deterministic.
356    ///
357    /// The consistency guarantee of the output depends on the network's failure policy
358    /// ([`NetworkFor::ConsistencyGuarantee`]). Policies like `fail_stop` and
359    /// `lossy_delayed_forever` guarantee that every live member eventually materializes the same
360    /// elements, so the output is
361    /// [`EventualConsistency`](crate::location::cluster::EventualConsistency). A plain `lossy`
362    /// policy can drop individual messages for some members while delivering them to others, so
363    /// replicas may permanently diverge and the output only has
364    /// [`NoConsistency`].
365    ///
366    /// This is only available in deployment targets with static cluster
367    /// membership (legacy Hydro Deploy and simulation). There are no late
368    /// joiners in that context, so broadcast receivers are guaranteed to
369    /// get data from the start of the stream. On dynamic targets
370    /// (e.g. ECS), use [`Stream::broadcast`] instead.
371    ///
372    /// # Example
373    /// ```rust
374    /// # #[cfg(feature = "deploy")] {
375    /// # use hydro_lang::prelude::*;
376    /// # use futures::StreamExt;
377    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
378    /// let p1 = flow.process::<()>();
379    /// let workers: Cluster<()> = flow.cluster::<()>();
380    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![123]));
381    /// let on_worker = numbers.broadcast_closed(&workers, TCP.fail_stop().bincode());
382    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
383    /// // each of the 4 cluster members receives 123
384    /// # }, |mut stream| async move {
385    /// # let mut results = Vec::new();
386    /// # for _ in 0..4 {
387    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
388    /// # }
389    /// # results.sort();
390    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 123)", "(MemberId::<()>(1), 123)", "(MemberId::<()>(2), 123)", "(MemberId::<()>(3), 123)"]);
391    /// # }));
392    /// # }
393    /// ```
394    pub fn broadcast_closed<L2: 'a, N: NetworkFor<T>>(
395        self,
396        to: &Cluster<'a, L2>,
397        via: N,
398    ) -> Stream<
399        T,
400        Cluster<'a, L2, N::ConsistencyGuarantee>,
401        Unbounded,
402        <O as MinOrder<N::OrderingGuarantee>>::Min,
403        R,
404    >
405    where
406        T: Clone,
407        O: MinOrder<N::OrderingGuarantee>,
408    {
409        let cluster_ids = ClusterIds {
410            key: to.key,
411            _phantom: PhantomData,
412        };
413        let member_ids = self.location.source_iter(q!(cluster_ids
414            .iter()
415            .map(|id| MemberId::from_tagless(id.clone()))));
416
417        // Late joiners will receive no data from this broadcast, which is
418        // future-monotone and eventually consistent (a safe under-approximation).
419        self.cross_product(member_ids)
420            .map(q!(|(data, member_id)| (member_id, data)))
421            .into_keyed()
422            .demux(to, via)
423            .assert_has_consistency_of_trusted(manual_proof!(
424                /// With a network whose failure policy delivers the same messages to every live
425                /// member (tracked by `NetworkFor::ConsistencyGuarantee`), a closed broadcast
426                /// will materialize the same elements on each member.
427            ))
428    }
429
430    /// Sends the elements of this stream to an external (non-Hydro) process, using [`bincode`]
431    /// serialization. The external process can receive these elements by establishing a TCP
432    /// connection and decoding using [`tokio_util::codec::LengthDelimitedCodec`].
433    ///
434    /// # Example
435    /// ```rust
436    /// # #[cfg(feature = "deploy")] {
437    /// # use hydro_lang::prelude::*;
438    /// # use futures::StreamExt;
439    /// # tokio_test::block_on(async move {
440    /// let mut flow = FlowBuilder::new();
441    /// let process = flow.process::<()>();
442    /// let numbers: Stream<_, Process<_>, Bounded> = process.source_iter(q!(vec![1, 2, 3]));
443    /// let external = flow.external::<()>();
444    /// let external_handle = numbers.send_bincode_external(&external);
445    ///
446    /// let mut deployment = hydro_deploy::Deployment::new();
447    /// let nodes = flow
448    ///     .with_process(&process, deployment.Localhost())
449    ///     .with_external(&external, deployment.Localhost())
450    ///     .deploy(&mut deployment);
451    ///
452    /// deployment.deploy().await.unwrap();
453    /// // establish the TCP connection
454    /// let mut external_recv_stream = nodes.connect(external_handle).await;
455    /// deployment.start().await.unwrap();
456    ///
457    /// for w in 1..=3 {
458    ///     assert_eq!(external_recv_stream.next().await, Some(w));
459    /// }
460    /// # });
461    /// # }
462    /// ```
463    pub fn send_bincode_external<L2>(
464        self,
465        other: &External<'_, L2>,
466    ) -> ExternalBincodeStream<T, O, R>
467    where
468        T: Serialize + DeserializeOwned,
469    {
470        let external_port_id =
471            self.register_serialized_external_port(other, serialize_bincode::<T>(false));
472
473        ExternalBincodeStream {
474            process_key: other.key,
475            port_id: external_port_id,
476            _phantom: PhantomData,
477        }
478    }
479
480    // TODO: Add a codec-parameterized external stream handle once deployment supports custom codecs.
481    fn register_serialized_external_port<L2>(
482        self,
483        other: &External<'_, L2>,
484        serialize_pipeline: syn::Expr,
485    ) -> ExternalPortId {
486        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
487
488        let external_port_id = flow_state_borrow.next_external_port();
489
490        flow_state_borrow.push_root(HydroRoot::SendExternal {
491            to_external_key: other.key,
492            to_port_id: external_port_id,
493            to_many: false,
494            unpaired: true,
495            serialize_fn: Some(serialize_pipeline.into()),
496            instantiate_fn: DebugInstantiate::Building,
497            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
498            op_metadata: HydroIrOpMetadata::new(),
499        });
500
501        external_port_id
502    }
503
504    #[cfg(feature = "sim")]
505    /// Sets up a bincode-encoded simulation output port for this stream, allowing test code to
506    /// receive elements sent to this stream during simulation. Use [`Stream::sim_output_with`] to
507    /// select another codec.
508    pub fn sim_output(self) -> SimReceiver<T, O, R>
509    where
510        T: Serialize + DeserializeOwned,
511    {
512        self.sim_output_with::<crate::sim::codec::BincodeCodec>()
513    }
514
515    #[cfg(feature = "sim")]
516    /// Sets up a simulation output port using the codec `C`, allowing test code to receive
517    /// elements sent to this stream during simulation: `stream.sim_output_with::<MyCodec>()`.
518    /// Custom codecs implement [`SimCodec`](crate::sim::codec::SimCodec), which documents
519    /// where they must be defined.
520    pub fn sim_output_with<C>(self) -> SimReceiver<T, O, R>
521    where
522        C: crate::sim::codec::SimCodec<T>,
523    {
524        let external_location: External<'a, ()> = External {
525            key: LocationKey::FIRST,
526            flow_state: self.location.flow_state().clone(),
527            _phantom: PhantomData,
528        };
529
530        let external_port_id = self.register_serialized_external_port(
531            &external_location,
532            crate::sim::codec::staged_serialize::<T, C>(),
533        );
534
535        SimReceiver(external_port_id, PhantomData, C::decode)
536    }
537}
538
539impl<'a, T, L: Location<'a>, B: Boundedness> Stream<T, L, B, TotalOrder, ExactlyOnce> {
540    /// Creates an external output for embedded deployment mode.
541    ///
542    /// The `name` parameter specifies the name of the field in the generated
543    /// `EmbeddedOutputs` struct that will receive elements from this stream.
544    /// The generated function will accept an `EmbeddedOutputs` struct with an
545    /// `impl FnMut(T)` field with this name.
546    pub fn embedded_output(self, name: impl Into<String>) {
547        let ident = syn::Ident::new(&name.into(), proc_macro2::Span::call_site());
548
549        self.location
550            .flow_state()
551            .borrow_mut()
552            .push_root(HydroRoot::EmbeddedOutput {
553                ident,
554                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
555                op_metadata: HydroIrOpMetadata::new(),
556            });
557    }
558}
559
560impl<'a, T, L, L2, B: Boundedness, O: Ordering, R: Retries>
561    Stream<(MemberId<L2>, T), Process<'a, L>, B, O, R>
562{
563    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
564    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
565    /// using [`bincode`] to serialize/deserialize messages.
566    ///
567    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
568    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
569    /// this API allows precise targeting of specific cluster members rather than broadcasting to
570    /// all members.
571    ///
572    /// # Example
573    /// ```rust
574    /// # #[cfg(feature = "deploy")] {
575    /// # use hydro_lang::prelude::*;
576    /// # use futures::StreamExt;
577    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
578    /// let p1 = flow.process::<()>();
579    /// let workers: Cluster<()> = flow.cluster::<()>();
580    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
581    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
582    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
583    ///     .demux_bincode(&workers);
584    /// # on_worker.send_bincode(&p2).entries()
585    /// // if there are 4 members in the cluster, each receives one element
586    /// // - MemberId::<()>(0): [0]
587    /// // - MemberId::<()>(1): [1]
588    /// // - MemberId::<()>(2): [2]
589    /// // - MemberId::<()>(3): [3]
590    /// # }, |mut stream| async move {
591    /// # let mut results = Vec::new();
592    /// # for w in 0..4 {
593    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
594    /// # }
595    /// # results.sort();
596    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
597    /// # }));
598    /// # }
599    /// ```
600    pub fn demux_bincode(
601        self,
602        other: &Cluster<'a, L2>,
603    ) -> Stream<T, Cluster<'a, L2>, Unbounded, O, R>
604    where
605        T: Serialize + DeserializeOwned,
606    {
607        self.demux(other, TCP.fail_stop().bincode())
608    }
609
610    /// Sends elements of this stream to specific members of a cluster, identified by a [`MemberId`],
611    /// using the configuration in `via` to set up the message transport.
612    ///
613    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
614    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
615    /// this API allows precise targeting of specific cluster members rather than broadcasting to
616    /// all members.
617    ///
618    /// # Example
619    /// ```rust
620    /// # #[cfg(feature = "deploy")] {
621    /// # use hydro_lang::prelude::*;
622    /// # use futures::StreamExt;
623    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
624    /// let p1 = flow.process::<()>();
625    /// let workers: Cluster<()> = flow.cluster::<()>();
626    /// let numbers: Stream<_, Process<_>, _> = p1.source_iter(q!(vec![0, 1, 2, 3]));
627    /// let on_worker: Stream<_, Cluster<_>, _> = numbers
628    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)))
629    ///     .demux(&workers, TCP.fail_stop().bincode());
630    /// # on_worker.send(&p2, TCP.fail_stop().bincode()).entries()
631    /// // if there are 4 members in the cluster, each receives one element
632    /// // - MemberId::<()>(0): [0]
633    /// // - MemberId::<()>(1): [1]
634    /// // - MemberId::<()>(2): [2]
635    /// // - MemberId::<()>(3): [3]
636    /// # }, |mut stream| async move {
637    /// # let mut results = Vec::new();
638    /// # for w in 0..4 {
639    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
640    /// # }
641    /// # results.sort();
642    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 0)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 2)", "(MemberId::<()>(3), 3)"]);
643    /// # }));
644    /// # }
645    /// ```
646    pub fn demux<N: NetworkFor<T>>(
647        self,
648        to: &Cluster<'a, L2>,
649        via: N,
650    ) -> Stream<
651        T,
652        Cluster<'a, L2, NoConsistency>,
653        Unbounded,
654        <O as MinOrder<N::OrderingGuarantee>>::Min,
655        R,
656    >
657    where
658        O: MinOrder<N::OrderingGuarantee>,
659    {
660        self.into_keyed().demux(to, via)
661    }
662}
663
664impl<'a, T, L, B: Boundedness> Stream<T, Process<'a, L>, B, TotalOrder, ExactlyOnce> {
665    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
666    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
667    /// [`bincode`] to serialize/deserialize messages.
668    ///
669    /// This provides load balancing by evenly distributing work across cluster members. The
670    /// distribution is deterministic based on element order - the first element goes to member 0,
671    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
672    ///
673    /// # Non-Determinism
674    /// The set of cluster members may asynchronously change over time. Each element is distributed
675    /// based on the current cluster membership _at that point in time_. Depending on when cluster
676    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
677    /// membership is stable, the order of members in the round-robin pattern may change across runs.
678    ///
679    /// # Ordering Requirements
680    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
681    /// order of messages and retries affects the round-robin pattern.
682    ///
683    /// # Example
684    /// ```rust
685    /// # #[cfg(feature = "deploy")] {
686    /// # use hydro_lang::prelude::*;
687    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
688    /// # use futures::StreamExt;
689    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
690    /// let p1 = flow.process::<()>();
691    /// let workers: Cluster<()> = flow.cluster::<()>();
692    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
693    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers, nondet!(/** assuming stable membership */));
694    /// on_worker.send_bincode(&p2)
695    /// # .first().values() // we use first to assert that each member gets one element
696    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
697    /// // - MemberId::<()>(?): [1]
698    /// // - MemberId::<()>(?): [2]
699    /// // - MemberId::<()>(?): [3]
700    /// // - MemberId::<()>(?): [4]
701    /// # }, |mut stream| async move {
702    /// # let mut results = Vec::new();
703    /// # for w in 0..4 {
704    /// #     results.push(stream.next().await.unwrap());
705    /// # }
706    /// # results.sort();
707    /// # assert_eq!(results, vec![1, 2, 3, 4]);
708    /// # }));
709    /// # }
710    /// ```
711    pub fn round_robin_bincode<L2: 'a>(
712        self,
713        other: &Cluster<'a, L2>,
714        nondet_membership: NonDet,
715    ) -> Stream<T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
716    where
717        T: Serialize + DeserializeOwned,
718    {
719        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
720    }
721
722    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
723    /// the configuration in `via` to set up the message transport.
724    ///
725    /// This provides load balancing by evenly distributing work across cluster members. The
726    /// distribution is deterministic based on element order - the first element goes to member 0,
727    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
728    ///
729    /// # Non-Determinism
730    /// The set of cluster members may asynchronously change over time. Each element is distributed
731    /// based on the current cluster membership _at that point in time_. Depending on when cluster
732    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
733    /// membership is stable, the order of members in the round-robin pattern may change across runs.
734    ///
735    /// # Ordering Requirements
736    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
737    /// order of messages and retries affects the round-robin pattern.
738    ///
739    /// # Example
740    /// ```rust
741    /// # #[cfg(feature = "deploy")] {
742    /// # use hydro_lang::prelude::*;
743    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce};
744    /// # use futures::StreamExt;
745    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
746    /// let p1 = flow.process::<()>();
747    /// let workers: Cluster<()> = flow.cluster::<()>();
748    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(vec![1, 2, 3, 4]));
749    /// let on_worker: Stream<_, Cluster<_>, _> = numbers.round_robin(&workers, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
750    /// on_worker.send(&p2, TCP.fail_stop().bincode())
751    /// # .first().values() // we use first to assert that each member gets one element
752    /// // with 4 cluster members, elements are distributed (with a non-deterministic round-robin order):
753    /// // - MemberId::<()>(?): [1]
754    /// // - MemberId::<()>(?): [2]
755    /// // - MemberId::<()>(?): [3]
756    /// // - MemberId::<()>(?): [4]
757    /// # }, |mut stream| async move {
758    /// # let mut results = Vec::new();
759    /// # for w in 0..4 {
760    /// #     results.push(stream.next().await.unwrap());
761    /// # }
762    /// # results.sort();
763    /// # assert_eq!(results, vec![1, 2, 3, 4]);
764    /// # }));
765    /// # }
766    /// ```
767    pub fn round_robin<L2: 'a, N: NetworkFor<T>>(
768        self,
769        to: &Cluster<'a, L2>,
770        via: N,
771        nondet_membership: NonDet,
772    ) -> Stream<T, Cluster<'a, L2>, Unbounded, N::OrderingGuarantee, ExactlyOnce> {
773        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
774        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
775        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
776        // can script the membership snapshot and the element batching independently.
777        let ids = track_membership(self.location.source_cluster_membership_stream(
778            to,
779            nondet!(/** dropped prefixes don't affect broadcast */),
780        ));
781        sliced! {
782            let members_snapshot = use::snapshot(ids, nondet!(
783                /// membership timing is captured by the caller's guard
784                nondet_membership
785            ));
786            let elements = use::batch(self.enumerate(), nondet!(
787                /// batching timing is captured by the caller's guard
788                nondet_membership
789            ));
790
791            let current_members = members_snapshot
792                .filter(q!(|b| *b))
793                .keys()
794                .assume_ordering::<TotalOrder>(nondet!(/** membership timing is captured by the caller guard */ nondet_membership))
795                .collect_vec();
796
797            elements
798                .cross_singleton(current_members)
799                .filter_map(q!(|(data, members)| {
800                    if members.is_empty() {
801                        None
802                    } else {
803                        Some((members[data.0 % members.len()].clone(), data.1))
804                    }
805                }))
806        }
807        .demux(to, via)
808    }
809}
810
811impl<'a, T, L, B: Boundedness, C: Consistency>
812    Stream<T, Cluster<'a, L, C>, B, TotalOrder, ExactlyOnce>
813{
814    #[deprecated = "use Stream::round_robin(..., TCP.fail_stop().bincode()) instead"]
815    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
816    /// [`bincode`] to serialize/deserialize messages.
817    ///
818    /// This provides load balancing by evenly distributing work across cluster members. The
819    /// distribution is deterministic based on element order - the first element goes to member 0,
820    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
821    ///
822    /// # Non-Determinism
823    /// The set of cluster members may asynchronously change over time. Each element is distributed
824    /// based on the current cluster membership _at that point in time_. Depending on when cluster
825    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
826    /// membership is stable, the order of members in the round-robin pattern may change across runs.
827    ///
828    /// # Ordering Requirements
829    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
830    /// order of messages and retries affects the round-robin pattern.
831    ///
832    /// # Example
833    /// ```rust
834    /// # #[cfg(feature = "deploy")] {
835    /// # use hydro_lang::prelude::*;
836    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce, NoOrder};
837    /// # use hydro_lang::location::MemberId;
838    /// # use futures::StreamExt;
839    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
840    /// let p1 = flow.process::<()>();
841    /// let workers1: Cluster<()> = flow.cluster::<()>();
842    /// let workers2: Cluster<()> = flow.cluster::<()>();
843    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(0..=16));
844    /// let on_worker1: Stream<_, Cluster<_>, _> = numbers.round_robin_bincode(&workers1, nondet!(/** assuming stable membership */));
845    /// let on_worker2: Stream<_, Cluster<_>, _> = on_worker1.round_robin_bincode(&workers2, nondet!(/** assuming stable membership */)).entries().assume_ordering(nondet!(/** assuming stable membership */));
846    /// on_worker2.send_bincode(&p2)
847    /// # .entries()
848    /// # .map(q!(|(w2, (w1, v))| ((w2, w1), v)))
849    /// # }, |mut stream| async move {
850    /// # let mut results = Vec::new();
851    /// # let mut locations = std::collections::HashSet::new();
852    /// # for w in 0..=16 {
853    /// #     let (location, v) = stream.next().await.unwrap();
854    /// #     locations.insert(location);
855    /// #     results.push(v);
856    /// # }
857    /// # results.sort();
858    /// # assert_eq!(results, (0..=16).collect::<Vec<_>>());
859    /// # assert_eq!(locations.len(), 16);
860    /// # }));
861    /// # }
862    /// ```
863    pub fn round_robin_bincode<L2: 'a>(
864        self,
865        other: &Cluster<'a, L2>,
866        nondet_membership: NonDet,
867    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, TotalOrder, ExactlyOnce>
868    where
869        T: Serialize + DeserializeOwned,
870    {
871        self.round_robin(other, TCP.fail_stop().bincode(), nondet_membership)
872    }
873
874    /// Distributes elements of this stream to cluster members in a round-robin fashion, using
875    /// the configuration in `via` to set up the message transport.
876    ///
877    /// This provides load balancing by evenly distributing work across cluster members. The
878    /// distribution is deterministic based on element order - the first element goes to member 0,
879    /// the second to member 1, and so on, wrapping around when reaching the end of the member list.
880    ///
881    /// # Non-Determinism
882    /// The set of cluster members may asynchronously change over time. Each element is distributed
883    /// based on the current cluster membership _at that point in time_. Depending on when cluster
884    /// members join and leave, the round-robin pattern will change. Furthermore, even when the
885    /// membership is stable, the order of members in the round-robin pattern may change across runs.
886    ///
887    /// # Ordering Requirements
888    /// This method is only available on streams with [`TotalOrder`] and [`ExactlyOnce`], since the
889    /// order of messages and retries affects the round-robin pattern.
890    ///
891    /// # Example
892    /// ```rust
893    /// # #[cfg(feature = "deploy")] {
894    /// # use hydro_lang::prelude::*;
895    /// # use hydro_lang::live_collections::stream::{TotalOrder, ExactlyOnce, NoOrder};
896    /// # use hydro_lang::location::MemberId;
897    /// # use futures::StreamExt;
898    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
899    /// let p1 = flow.process::<()>();
900    /// let workers1: Cluster<()> = flow.cluster::<()>();
901    /// let workers2: Cluster<()> = flow.cluster::<()>();
902    /// let numbers: Stream<_, Process<_>, _, TotalOrder, ExactlyOnce> = p1.source_iter(q!(0..=16));
903    /// let on_worker1: Stream<_, Cluster<_>, _> = numbers.round_robin(&workers1, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
904    /// let on_worker2: Stream<_, Cluster<_>, _> = on_worker1.round_robin(&workers2, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */)).entries().assume_ordering(nondet!(/** assuming stable membership */));
905    /// on_worker2.send(&p2, TCP.fail_stop().bincode())
906    /// # .entries()
907    /// # .map(q!(|(w2, (w1, v))| ((w2, w1), v)))
908    /// # }, |mut stream| async move {
909    /// # let mut results = Vec::new();
910    /// # let mut locations = std::collections::HashSet::new();
911    /// # for w in 0..=16 {
912    /// #     let (location, v) = stream.next().await.unwrap();
913    /// #     locations.insert(location);
914    /// #     results.push(v);
915    /// # }
916    /// # results.sort();
917    /// # assert_eq!(results, (0..=16).collect::<Vec<_>>());
918    /// # assert_eq!(locations.len(), 16);
919    /// # }));
920    /// # }
921    /// ```
922    pub fn round_robin<L2: 'a, N: NetworkFor<T>>(
923        self,
924        to: &Cluster<'a, L2>,
925        via: N,
926        nondet_membership: NonDet,
927    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, N::OrderingGuarantee, ExactlyOnce>
928    {
929        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
930        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
931        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
932        // can script the membership snapshot and the element batching independently.
933        let ids = track_membership(self.location.source_cluster_membership_stream(
934            to,
935            nondet!(/** dropped prefixes don't affect broadcast */),
936        ));
937        sliced! {
938            let members_snapshot = use::snapshot(ids, nondet!(
939                /// membership timing is captured by the caller's guard
940                nondet_membership
941            ));
942            let elements = use::batch(self.enumerate(), nondet!(
943                /// batching timing is captured by the caller's guard
944                nondet_membership
945            ));
946
947            let current_members = members_snapshot
948                .filter(q!(|b| *b))
949                .keys()
950                .assume_ordering::<TotalOrder>(nondet!(/** membership timing is captured by the caller guard */ nondet_membership))
951                .collect_vec();
952
953            elements
954                .cross_singleton(current_members)
955                .filter_map(q!(|(data, members)| {
956                    if members.is_empty() {
957                        None
958                    } else {
959                        Some((members[data.0 % members.len()].clone(), data.1))
960                    }
961                }))
962        }
963        .demux(to, via)
964    }
965}
966
967impl<'a, T, L, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
968    Stream<T, Cluster<'a, L, C>, B, O, R>
969{
970    #[deprecated = "use Stream::send(..., TCP.fail_stop().bincode()) instead"]
971    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
972    /// using [`bincode`] to serialize/deserialize messages.
973    ///
974    /// Each cluster member sends its local stream elements, and they are collected at the destination
975    /// as a [`KeyedStream`] where keys identify the source cluster member.
976    ///
977    /// # Example
978    /// ```rust
979    /// # #[cfg(feature = "deploy")] {
980    /// # use hydro_lang::prelude::*;
981    /// # use futures::StreamExt;
982    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
983    /// let workers: Cluster<()> = flow.cluster::<()>();
984    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
985    /// let all_received = numbers.send_bincode(&process); // KeyedStream<MemberId<()>, i32, ...>
986    /// # all_received.entries()
987    /// # }, |mut stream| async move {
988    /// // if there are 4 members in the cluster, we should receive 4 elements
989    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
990    /// # let mut results = Vec::new();
991    /// # for w in 0..4 {
992    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
993    /// # }
994    /// # results.sort();
995    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
996    /// # }));
997    /// # }
998    /// ```
999    ///
1000    /// If you don't need to know the source for each element, you can use `.values()`
1001    /// to get just the data:
1002    /// ```rust
1003    /// # #[cfg(feature = "deploy")] {
1004    /// # use hydro_lang::prelude::*;
1005    /// # use hydro_lang::live_collections::stream::NoOrder;
1006    /// # use futures::StreamExt;
1007    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1008    /// # let workers: Cluster<()> = flow.cluster::<()>();
1009    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1010    /// let values: Stream<i32, _, _, NoOrder> = numbers.send_bincode(&process).values();
1011    /// # values
1012    /// # }, |mut stream| async move {
1013    /// # let mut results = Vec::new();
1014    /// # for w in 0..4 {
1015    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1016    /// # }
1017    /// # results.sort();
1018    /// // if there are 4 members in the cluster, we should receive 4 elements
1019    /// // 1, 1, 1, 1
1020    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
1021    /// # }));
1022    /// # }
1023    /// ```
1024    pub fn send_bincode<L2>(
1025        self,
1026        other: &Process<'a, L2>,
1027    ) -> KeyedStream<MemberId<L>, T, Process<'a, L2>, Unbounded, O, R>
1028    where
1029        T: Serialize + DeserializeOwned,
1030    {
1031        self.send(other, TCP.fail_stop().bincode())
1032    }
1033
1034    /// "Moves" elements of this stream from a cluster to a process by sending them over the network,
1035    /// using the configuration in `via` to set up the message transport.
1036    ///
1037    /// Each cluster member sends its local stream elements, and they are collected at the destination
1038    /// as a [`KeyedStream`] where keys identify the source cluster member.
1039    ///
1040    /// # Example
1041    /// ```rust
1042    /// # #[cfg(feature = "deploy")] {
1043    /// # use hydro_lang::prelude::*;
1044    /// # use futures::StreamExt;
1045    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1046    /// let workers: Cluster<()> = flow.cluster::<()>();
1047    /// let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1048    /// let all_received = numbers.send(&process, TCP.fail_stop().bincode()); // KeyedStream<MemberId<()>, i32, ...>
1049    /// # all_received.entries()
1050    /// # }, |mut stream| async move {
1051    /// // if there are 4 members in the cluster, we should receive 4 elements
1052    /// // { MemberId::<()>(0): [1], MemberId::<()>(1): [1], MemberId::<()>(2): [1], MemberId::<()>(3): [1] }
1053    /// # let mut results = Vec::new();
1054    /// # for w in 0..4 {
1055    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1056    /// # }
1057    /// # results.sort();
1058    /// # assert_eq!(results, vec!["(MemberId::<()>(0), 1)", "(MemberId::<()>(1), 1)", "(MemberId::<()>(2), 1)", "(MemberId::<()>(3), 1)"]);
1059    /// # }));
1060    /// # }
1061    /// ```
1062    ///
1063    /// If you don't need to know the source for each element, you can use `.values()`
1064    /// to get just the data:
1065    /// ```rust
1066    /// # #[cfg(feature = "deploy")] {
1067    /// # use hydro_lang::prelude::*;
1068    /// # use hydro_lang::live_collections::stream::NoOrder;
1069    /// # use futures::StreamExt;
1070    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, process| {
1071    /// # let workers: Cluster<()> = flow.cluster::<()>();
1072    /// # let numbers: Stream<_, Cluster<_>, _> = workers.source_iter(q!(vec![1]));
1073    /// let values: Stream<i32, _, _, NoOrder> =
1074    ///     numbers.send(&process, TCP.fail_stop().bincode()).values();
1075    /// # values
1076    /// # }, |mut stream| async move {
1077    /// # let mut results = Vec::new();
1078    /// # for w in 0..4 {
1079    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1080    /// # }
1081    /// # results.sort();
1082    /// // if there are 4 members in the cluster, we should receive 4 elements
1083    /// // 1, 1, 1, 1
1084    /// # assert_eq!(results, vec!["1", "1", "1", "1"]);
1085    /// # }));
1086    /// # }
1087    /// ```
1088    pub fn send<L2, N: NetworkFor<T>>(
1089        self,
1090        to: &Process<'a, L2>,
1091        via: N,
1092    ) -> KeyedStream<
1093        MemberId<L>,
1094        T,
1095        Process<'a, L2>,
1096        Unbounded,
1097        <O as MinOrder<N::OrderingGuarantee>>::Min,
1098        R,
1099    >
1100    where
1101        O: MinOrder<N::OrderingGuarantee>,
1102    {
1103        let name = via.name();
1104        if to.multiversioned() && name.is_none() {
1105            panic!(
1106                "Cannot send to a multiversioned location without a channel name. Please provide a name for the network."
1107            );
1108        }
1109
1110        let (serialize, deserialize) = if N::is_embedded() {
1111            (
1112                NetworkSend::Embedded {
1113                    tag: None,
1114                    element_type: quote_type::<T>().into(),
1115                },
1116                NetworkRecv::Embedded {
1117                    tag: Some(quote_type::<L>().into()),
1118                    element_type: quote_type::<T>().into(),
1119                },
1120            )
1121        } else {
1122            (
1123                NetworkSend::Custom {
1124                    serialize_fn: Some(N::serialize_thunk(false).into()),
1125                },
1126                NetworkRecv::Custom {
1127                    deserialize_fn: Some(N::deserialize_thunk(Some(&quote_type::<L>())).into()),
1128                },
1129            )
1130        };
1131
1132        let raw_stream: Stream<
1133            (MemberId<L>, T),
1134            Process<'a, L2>,
1135            Unbounded,
1136            <O as MinOrder<N::OrderingGuarantee>>::Min,
1137            R,
1138        > = Stream::new(
1139            to.clone(),
1140            HydroNode::Network {
1141                name: name.map(ToOwned::to_owned),
1142                networking_info: N::networking_info(),
1143                serialize,
1144                deserialize,
1145                instantiate_fn: DebugInstantiate::Building,
1146                input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1147                metadata: to.new_node_metadata(Stream::<
1148                    (MemberId<L>, T),
1149                    Process<'a, L2>,
1150                    Unbounded,
1151                    <O as MinOrder<N::OrderingGuarantee>>::Min,
1152                    R,
1153                >::collection_kind()),
1154            },
1155        );
1156
1157        raw_stream.into_keyed()
1158    }
1159
1160    #[deprecated = "use Stream::broadcast(..., TCP.fail_stop().bincode()) instead"]
1161    /// Broadcasts elements of this stream at each source member to all members of a destination
1162    /// cluster, using [`bincode`] to serialize/deserialize messages.
1163    ///
1164    /// Each source member sends each of its stream elements to **every** member of the cluster
1165    /// based on its latest membership information. Unlike [`Stream::demux_bincode`], which requires
1166    /// `(MemberId, T)` tuples to target specific members, `broadcast_bincode` takes a stream of
1167    /// **only data elements** and sends each element to all cluster members.
1168    ///
1169    /// # Non-Determinism
1170    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1171    /// to the current cluster members known _at that point in time_ at the source member. Depending
1172    /// on when each source member is notified of membership changes, it will broadcast each element
1173    /// to different members.
1174    ///
1175    /// # Example
1176    /// ```rust
1177    /// # #[cfg(feature = "deploy")] {
1178    /// # use hydro_lang::prelude::*;
1179    /// # use hydro_lang::location::MemberId;
1180    /// # use futures::StreamExt;
1181    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1182    /// # type Source = ();
1183    /// # type Destination = ();
1184    /// let source: Cluster<Source> = flow.cluster::<Source>();
1185    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1186    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1187    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast_bincode(&destination, nondet!(/** assuming stable membership */));
1188    /// # on_destination.entries().send_bincode(&p2).entries()
1189    /// // if there are 4 members in the desination, each receives one element from each source member
1190    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1191    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1192    /// // - ...
1193    /// # }, |mut stream| async move {
1194    /// # let mut results = Vec::new();
1195    /// # for w in 0..16 {
1196    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1197    /// # }
1198    /// # results.sort();
1199    /// # assert_eq!(results, vec![
1200    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1201    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1202    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1203    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1204    /// # ]);
1205    /// # }));
1206    /// # }
1207    /// ```
1208    pub fn broadcast_bincode<L2: 'a>(
1209        self,
1210        other: &Cluster<'a, L2>,
1211        nondet_membership: NonDet,
1212    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1213    where
1214        T: Clone + Serialize + DeserializeOwned,
1215    {
1216        self.broadcast(other, TCP.fail_stop().bincode(), nondet_membership)
1217    }
1218
1219    /// Broadcasts elements of this stream at each source member to all members of a destination
1220    /// cluster, using the configuration in `via` to set up the message transport.
1221    ///
1222    /// Each source member sends each of its stream elements to **every** member of the cluster
1223    /// based on its latest membership information. Unlike [`Stream::demux`], which requires
1224    /// `(MemberId, T)` tuples to target specific members, `broadcast` takes a stream of
1225    /// **only data elements** and sends each element to all cluster members.
1226    ///
1227    /// # Non-Determinism
1228    /// The set of cluster members may asynchronously change over time. Each element is only broadcast
1229    /// to the current cluster members known _at that point in time_ at the source member. Depending
1230    /// on when each source member is notified of membership changes, it will broadcast each element
1231    /// to different members.
1232    ///
1233    /// # Example
1234    /// ```rust
1235    /// # #[cfg(feature = "deploy")] {
1236    /// # use hydro_lang::prelude::*;
1237    /// # use hydro_lang::location::MemberId;
1238    /// # use futures::StreamExt;
1239    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1240    /// # type Source = ();
1241    /// # type Destination = ();
1242    /// let source: Cluster<Source> = flow.cluster::<Source>();
1243    /// let numbers: Stream<_, Cluster<Source>, _> = source.source_iter(q!(vec![123]));
1244    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1245    /// let on_destination: KeyedStream<MemberId<Source>, _, Cluster<Destination>, _> = numbers.broadcast(&destination, TCP.fail_stop().bincode(), nondet!(/** assuming stable membership */));
1246    /// # on_destination.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1247    /// // if there are 4 members in the desination, each receives one element from each source member
1248    /// // - Destination(0): { Source(0): [123], Source(1): [123], ... }
1249    /// // - Destination(1): { Source(0): [123], Source(1): [123], ... }
1250    /// // - ...
1251    /// # }, |mut stream| async move {
1252    /// # let mut results = Vec::new();
1253    /// # for w in 0..16 {
1254    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1255    /// # }
1256    /// # results.sort();
1257    /// # assert_eq!(results, vec![
1258    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 123))", "(MemberId::<()>(0), (MemberId::<()>(1), 123))", "(MemberId::<()>(0), (MemberId::<()>(2), 123))", "(MemberId::<()>(0), (MemberId::<()>(3), 123))",
1259    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 123))", "(MemberId::<()>(1), (MemberId::<()>(1), 123))", "(MemberId::<()>(1), (MemberId::<()>(2), 123))", "(MemberId::<()>(1), (MemberId::<()>(3), 123))",
1260    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 123))", "(MemberId::<()>(2), (MemberId::<()>(1), 123))", "(MemberId::<()>(2), (MemberId::<()>(2), 123))", "(MemberId::<()>(2), (MemberId::<()>(3), 123))",
1261    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 123))", "(MemberId::<()>(3), (MemberId::<()>(1), 123))", "(MemberId::<()>(3), (MemberId::<()>(2), 123))", "(MemberId::<()>(3), (MemberId::<()>(3), 123))"
1262    /// # ]);
1263    /// # }));
1264    /// # }
1265    /// ```
1266    pub fn broadcast<L2: 'a, N: NetworkFor<T>>(
1267        self,
1268        to: &Cluster<'a, L2>,
1269        via: N,
1270        nondet_membership: NonDet,
1271    ) -> KeyedStream<
1272        MemberId<L>,
1273        T,
1274        Cluster<'a, L2>,
1275        Unbounded,
1276        <O as MinOrder<N::OrderingGuarantee>>::Min,
1277        R,
1278    >
1279    where
1280        T: Clone,
1281        O: MinOrder<N::OrderingGuarantee>,
1282    {
1283        // TODO(#1875): the membership snapshot below is over a `KeyedSingleton`, and keyed
1284        // sim hooks do not exist yet. Once they do, expose a composite hook payload here
1285        // (`NonDet<(Option<KeyedSnapshotHook<..>>, Option<BatchHook<T, O, R>>)>`) so tests
1286        // can script the membership snapshot and the element batching independently.
1287        let ids = track_membership(self.location.source_cluster_membership_stream(
1288            to,
1289            nondet!(/** dropped prefixes don't affect broadcast */),
1290        ));
1291        sliced! {
1292            let members_snapshot = use::snapshot(ids, nondet!(
1293                /// membership timing is captured by the caller's guard
1294                nondet_membership
1295            ));
1296            let elements = use::batch(self, nondet!(
1297                /// batching timing is captured by the caller's guard
1298                nondet_membership
1299            ));
1300
1301            let current_members = members_snapshot.filter(q!(|b| *b));
1302            elements.repeat_with_keys(current_members)
1303        }
1304        .demux(to, via)
1305    }
1306
1307    /// Broadcasts elements of this stream at each source member to all members of a destination
1308    /// cluster, assuming membership is closed (fixed at deploy time).
1309    ///
1310    /// Unlike [`Stream::broadcast`], this does not require a [`NonDet`] guard.
1311    /// The membership set is obtained from deploy metadata via [`ClusterIds`], making the
1312    /// broadcast fully deterministic.
1313    ///
1314    /// The consistency guarantee of the output depends on the network's failure policy
1315    /// ([`NetworkFor::ConsistencyGuarantee`]). Policies like `fail_stop` and
1316    /// `lossy_delayed_forever` guarantee that every live destination member eventually
1317    /// materializes the same elements from each source, so the output is
1318    /// [`EventualConsistency`](crate::location::cluster::EventualConsistency). A plain `lossy`
1319    /// policy can drop individual messages for some
1320    /// members while delivering them to others, so replicas may permanently diverge and the
1321    /// output only has [`NoConsistency`].
1322    ///
1323    /// This is only available in deployment targets with static cluster membership
1324    /// (legacy Hydro Deploy and simulation). On dynamic targets, use [`Stream::broadcast`].
1325    pub fn broadcast_closed<L2: 'a, N: NetworkFor<T>>(
1326        self,
1327        to: &Cluster<'a, L2>,
1328        via: N,
1329    ) -> KeyedStream<
1330        MemberId<L>,
1331        T,
1332        Cluster<'a, L2, N::ConsistencyGuarantee>,
1333        Unbounded,
1334        <O as MinOrder<N::OrderingGuarantee>>::Min,
1335        R,
1336    >
1337    where
1338        T: Clone,
1339        O: MinOrder<N::OrderingGuarantee>,
1340    {
1341        let cluster_ids = ClusterIds {
1342            key: to.key,
1343            _phantom: PhantomData,
1344        };
1345        let member_ids = self
1346            .location
1347            .source_iter(q!(cluster_ids
1348                .iter()
1349                .map(|id| MemberId::from_tagless(id.clone()))))
1350            .assert_has_consistency_of_trusted::<Cluster<'a, L, C>>(manual_proof!(
1351                /// ClusterIds is deploy-time metadata, identical on every cluster member.
1352            ));
1353
1354        self.cross_product(member_ids)
1355            .map(q!(|(data, member_id)| (member_id, data)))
1356            .into_keyed()
1357            .demux(to, via)
1358            .assert_has_consistency_of_trusted(manual_proof!(
1359                /// Closed broadcast with fixed membership: every source member sends to every
1360                /// destination member, and the network's failure policy (tracked by
1361                /// `NetworkFor::ConsistencyGuarantee`) delivers the same messages to every live
1362                /// member, so all destinations materialize the same elements.
1363            ))
1364    }
1365
1366    #[cfg(feature = "sim")]
1367    fn register_serialized_external_port<L2>(
1368        self,
1369        other: &External<'_, L2>,
1370        serialize_pipeline: syn::Expr,
1371    ) -> ExternalPortId {
1372        let mut flow_state_borrow = self.location.flow_state().borrow_mut();
1373
1374        let external_port_id = flow_state_borrow.next_external_port();
1375
1376        flow_state_borrow.push_root(HydroRoot::SendExternal {
1377            to_external_key: other.key,
1378            to_port_id: external_port_id,
1379            to_many: false,
1380            unpaired: true,
1381            serialize_fn: Some(serialize_pipeline.into()),
1382            instantiate_fn: DebugInstantiate::Building,
1383            input: Box::new(self.ir_node.replace(HydroNode::Placeholder)),
1384            op_metadata: HydroIrOpMetadata::new(),
1385        });
1386
1387        external_port_id
1388    }
1389
1390    #[cfg(feature = "sim")]
1391    /// Sets up a bincode-encoded simulation output port for this cluster stream, allowing test
1392    /// code to receive `(member_id, T)` pairs during simulation. Use
1393    /// [`Stream::sim_cluster_output_with`] to select another codec.
1394    pub fn sim_cluster_output(self) -> crate::sim::SimClusterReceiver<T, O, R>
1395    where
1396        T: Serialize + DeserializeOwned,
1397    {
1398        self.sim_cluster_output_with::<crate::sim::codec::BincodeCodec>()
1399    }
1400
1401    #[cfg(feature = "sim")]
1402    /// Sets up a simulation output port for this cluster stream using the codec `Codec`,
1403    /// allowing test code to receive `(member_id, T)` pairs during simulation:
1404    /// `stream.sim_cluster_output_with::<MyCodec>()`. Custom codecs implement
1405    /// [`SimCodec`](crate::sim::codec::SimCodec), which documents where they must be defined.
1406    pub fn sim_cluster_output_with<Codec>(self) -> crate::sim::SimClusterReceiver<T, O, R>
1407    where
1408        Codec: crate::sim::codec::SimCodec<T>,
1409    {
1410        let external_location: External<'a, ()> = External {
1411            key: LocationKey::FIRST,
1412            flow_state: self.location.flow_state().clone(),
1413            _phantom: PhantomData,
1414        };
1415
1416        let external_port_id = self.register_serialized_external_port(
1417            &external_location,
1418            crate::sim::codec::staged_serialize::<T, Codec>(),
1419        );
1420
1421        crate::sim::SimClusterReceiver(external_port_id, PhantomData, Codec::decode)
1422    }
1423}
1424
1425impl<'a, T, L, L2, B: Boundedness, C: Consistency, O: Ordering, R: Retries>
1426    Stream<(MemberId<L2>, T), Cluster<'a, L, C>, B, O, R>
1427{
1428    #[deprecated = "use Stream::demux(..., TCP.fail_stop().bincode()) instead"]
1429    /// Sends elements of this stream at each source member to specific members of a destination
1430    /// cluster, identified by a [`MemberId`], using [`bincode`] to serialize/deserialize messages.
1431    ///
1432    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1433    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast_bincode`],
1434    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1435    /// all members.
1436    ///
1437    /// Each cluster member sends its local stream elements, and they are collected at each
1438    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1439    ///
1440    /// # Example
1441    /// ```rust
1442    /// # #[cfg(feature = "deploy")] {
1443    /// # use hydro_lang::prelude::*;
1444    /// # use futures::StreamExt;
1445    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1446    /// # type Source = ();
1447    /// # type Destination = ();
1448    /// let source: Cluster<Source> = flow.cluster::<Source>();
1449    /// let to_send: Stream<_, Cluster<_>, _> = source
1450    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1451    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1452    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1453    /// let all_received = to_send.demux_bincode(&destination); // KeyedStream<MemberId<Source>, i32, ...>
1454    /// # all_received.entries().send_bincode(&p2).entries()
1455    /// # }, |mut stream| async move {
1456    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1457    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1458    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1459    /// // - ...
1460    /// # let mut results = Vec::new();
1461    /// # for w in 0..16 {
1462    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1463    /// # }
1464    /// # results.sort();
1465    /// # assert_eq!(results, vec![
1466    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1467    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1468    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1469    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1470    /// # ]);
1471    /// # }));
1472    /// # }
1473    /// ```
1474    pub fn demux_bincode(
1475        self,
1476        other: &Cluster<'a, L2>,
1477    ) -> KeyedStream<MemberId<L>, T, Cluster<'a, L2>, Unbounded, O, R>
1478    where
1479        T: Serialize + DeserializeOwned,
1480    {
1481        self.demux(other, TCP.fail_stop().bincode())
1482    }
1483
1484    /// Sends elements of this stream at each source member to specific members of a destination
1485    /// cluster, identified by a [`MemberId`], using the configuration in `via` to set up the
1486    /// message transport.
1487    ///
1488    /// Each element in the stream must be a tuple `(MemberId<L2>, T)` where the first element
1489    /// specifies which cluster member should receive the data. Unlike [`Stream::broadcast`],
1490    /// this API allows precise targeting of specific cluster members rather than broadcasting to
1491    /// all members.
1492    ///
1493    /// Each cluster member sends its local stream elements, and they are collected at each
1494    /// destination member as a [`KeyedStream`] where keys identify the source cluster member.
1495    ///
1496    /// # Example
1497    /// ```rust
1498    /// # #[cfg(feature = "deploy")] {
1499    /// # use hydro_lang::prelude::*;
1500    /// # use futures::StreamExt;
1501    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
1502    /// # type Source = ();
1503    /// # type Destination = ();
1504    /// let source: Cluster<Source> = flow.cluster::<Source>();
1505    /// let to_send: Stream<_, Cluster<_>, _> = source
1506    ///     .source_iter(q!(vec![0, 1, 2, 3]))
1507    ///     .map(q!(|x| (hydro_lang::location::MemberId::from_raw_id(x), x)));
1508    /// let destination: Cluster<Destination> = flow.cluster::<Destination>();
1509    /// let all_received = to_send.demux(&destination, TCP.fail_stop().bincode()); // KeyedStream<MemberId<Source>, i32, ...>
1510    /// # all_received.entries().send(&p2, TCP.fail_stop().bincode()).entries()
1511    /// # }, |mut stream| async move {
1512    /// // if there are 4 members in the destination cluster, each receives one message from each source member
1513    /// // - Destination(0): { Source(0): [0], Source(1): [0], ... }
1514    /// // - Destination(1): { Source(0): [1], Source(1): [1], ... }
1515    /// // - ...
1516    /// # let mut results = Vec::new();
1517    /// # for w in 0..16 {
1518    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
1519    /// # }
1520    /// # results.sort();
1521    /// # assert_eq!(results, vec![
1522    /// #   "(MemberId::<()>(0), (MemberId::<()>(0), 0))", "(MemberId::<()>(0), (MemberId::<()>(1), 0))", "(MemberId::<()>(0), (MemberId::<()>(2), 0))", "(MemberId::<()>(0), (MemberId::<()>(3), 0))",
1523    /// #   "(MemberId::<()>(1), (MemberId::<()>(0), 1))", "(MemberId::<()>(1), (MemberId::<()>(1), 1))", "(MemberId::<()>(1), (MemberId::<()>(2), 1))", "(MemberId::<()>(1), (MemberId::<()>(3), 1))",
1524    /// #   "(MemberId::<()>(2), (MemberId::<()>(0), 2))", "(MemberId::<()>(2), (MemberId::<()>(1), 2))", "(MemberId::<()>(2), (MemberId::<()>(2), 2))", "(MemberId::<()>(2), (MemberId::<()>(3), 2))",
1525    /// #   "(MemberId::<()>(3), (MemberId::<()>(0), 3))", "(MemberId::<()>(3), (MemberId::<()>(1), 3))", "(MemberId::<()>(3), (MemberId::<()>(2), 3))", "(MemberId::<()>(3), (MemberId::<()>(3), 3))"
1526    /// # ]);
1527    /// # }));
1528    /// # }
1529    /// ```
1530    pub fn demux<N: NetworkFor<T>>(
1531        self,
1532        to: &Cluster<'a, L2>,
1533        via: N,
1534    ) -> KeyedStream<
1535        MemberId<L>,
1536        T,
1537        Cluster<'a, L2, NoConsistency>,
1538        Unbounded,
1539        <O as MinOrder<N::OrderingGuarantee>>::Min,
1540        R,
1541    >
1542    where
1543        O: MinOrder<N::OrderingGuarantee>,
1544    {
1545        self.into_keyed().demux(to, via)
1546    }
1547}
1548
1549#[cfg(test)]
1550mod tests {
1551    #[cfg(feature = "sim")]
1552    use stageleft::q;
1553
1554    #[cfg(feature = "sim")]
1555    use crate::live_collections::sliced::sliced;
1556    #[cfg(feature = "sim")]
1557    use crate::location::{Location, MemberId};
1558    #[cfg(feature = "sim")]
1559    use crate::networking::TCP;
1560    #[cfg(feature = "sim")]
1561    use crate::nondet::nondet;
1562    #[cfg(feature = "sim")]
1563    use crate::prelude::FlowBuilder;
1564
1565    #[cfg(feature = "sim")]
1566    #[test]
1567    fn sim_send_bincode_o2o() {
1568        use crate::networking::TCP;
1569
1570        let mut flow = FlowBuilder::new();
1571        let node = flow.process::<()>();
1572        let node2 = flow.process::<()>();
1573
1574        let (in_send, input) = node.sim_input();
1575
1576        let out_recv = input
1577            .send(&node2, TCP.fail_stop().bincode())
1578            .batch(&node2.tick(), nondet!(/** test */))
1579            .count()
1580            .all_ticks()
1581            .sim_output();
1582
1583        let instances = flow.sim().exhaustive(async || {
1584            in_send.send(());
1585            in_send.send(());
1586            in_send.send(());
1587
1588            let received = out_recv.collect::<Vec<_>>().await;
1589            assert!(received.into_iter().sum::<usize>() == 3);
1590        });
1591
1592        assert_eq!(instances, 4); // 2^{3 - 1}
1593    }
1594
1595    #[cfg(feature = "sim")]
1596    #[test]
1597    fn sim_send_bincode_m2o() {
1598        let mut flow = FlowBuilder::new();
1599        let cluster = flow.cluster::<()>();
1600        let node = flow.process::<()>();
1601
1602        let input = cluster.source_iter(q!(vec![1]));
1603
1604        let out_recv = input
1605            .send(&node, TCP.fail_stop().bincode())
1606            .entries()
1607            .batch(&node.tick(), nondet!(/** test */))
1608            .all_ticks()
1609            .sim_output();
1610
1611        let instances = flow
1612            .sim()
1613            .with_cluster_size(&cluster, 4)
1614            .exhaustive(async || {
1615                out_recv
1616                    .assert_yields_only_unordered(vec![
1617                        (MemberId::from_raw_id(0), 1),
1618                        (MemberId::from_raw_id(1), 1),
1619                        (MemberId::from_raw_id(2), 1),
1620                        (MemberId::from_raw_id(3), 1),
1621                    ])
1622                    .await
1623            });
1624
1625        assert_eq!(instances, 75); // ∑ (k=1 to 4) S(4,k) × k! = 75
1626    }
1627
1628    #[cfg(feature = "sim")]
1629    #[test]
1630    fn sim_send_bincode_multiple_m2o() {
1631        let mut flow = FlowBuilder::new();
1632        let cluster1 = flow.cluster::<()>();
1633        let cluster2 = flow.cluster::<()>();
1634        let node = flow.process::<()>();
1635
1636        let out_recv_1 = cluster1
1637            .source_iter(q!(vec![1]))
1638            .send(&node, TCP.fail_stop().bincode())
1639            .entries()
1640            .sim_output();
1641
1642        let out_recv_2 = cluster2
1643            .source_iter(q!(vec![2]))
1644            .send(&node, TCP.fail_stop().bincode())
1645            .entries()
1646            .sim_output();
1647
1648        let instances = flow
1649            .sim()
1650            .with_cluster_size(&cluster1, 3)
1651            .with_cluster_size(&cluster2, 4)
1652            .exhaustive(async || {
1653                out_recv_1
1654                    .assert_yields_only_unordered(vec![
1655                        (MemberId::from_raw_id(0), 1),
1656                        (MemberId::from_raw_id(1), 1),
1657                        (MemberId::from_raw_id(2), 1),
1658                    ])
1659                    .await;
1660
1661                out_recv_2
1662                    .assert_yields_only_unordered(vec![
1663                        (MemberId::from_raw_id(0), 2),
1664                        (MemberId::from_raw_id(1), 2),
1665                        (MemberId::from_raw_id(2), 2),
1666                        (MemberId::from_raw_id(3), 2),
1667                    ])
1668                    .await;
1669            });
1670
1671        assert_eq!(instances, 1);
1672    }
1673
1674    #[cfg(feature = "sim")]
1675    #[test]
1676    fn sim_send_bincode_o2m() {
1677        let mut flow = FlowBuilder::new();
1678        let cluster = flow.cluster::<()>();
1679        let node = flow.process::<()>();
1680
1681        let input = node.source_iter(q!(vec![
1682            (MemberId::from_raw_id(0), 123),
1683            (MemberId::from_raw_id(1), 456),
1684        ]));
1685
1686        let out_recv = input
1687            .demux(&cluster, TCP.fail_stop().bincode())
1688            .map(q!(|x| x + 1))
1689            .send(&node, TCP.fail_stop().bincode())
1690            .entries()
1691            .sim_output();
1692
1693        flow.sim()
1694            .with_cluster_size(&cluster, 4)
1695            .exhaustive(async || {
1696                out_recv
1697                    .assert_yields_only_unordered(vec![
1698                        (MemberId::from_raw_id(0), 124),
1699                        (MemberId::from_raw_id(1), 457),
1700                    ])
1701                    .await
1702            });
1703    }
1704
1705    #[cfg(feature = "sim")]
1706    #[test]
1707    fn sim_broadcast_bincode_o2m() {
1708        let mut flow = FlowBuilder::new();
1709        let cluster = flow.cluster::<()>();
1710        let node = flow.process::<()>();
1711
1712        let input = node.source_iter(q!(vec![123, 456]));
1713
1714        let out_recv = input
1715            .broadcast(&cluster, TCP.fail_stop().bincode(), nondet!(/** test */))
1716            .map(q!(|x| x + 1))
1717            .send(&node, TCP.fail_stop().bincode())
1718            .entries()
1719            .sim_output();
1720
1721        let mut c_1_produced = false;
1722        let mut c_2_produced = false;
1723        let mut c_1_saw_457_but_not_124 = false;
1724
1725        flow.sim()
1726            .with_cluster_size(&cluster, 2)
1727            .exhaustive(async || {
1728                let all_out = out_recv.collect_sorted::<Vec<_>>().await;
1729
1730                // check that order is preserved
1731                if all_out.contains(&(MemberId::from_raw_id(0), 124)) {
1732                    assert!(all_out.contains(&(MemberId::from_raw_id(0), 457)));
1733                    c_1_produced = true;
1734                }
1735
1736                if all_out.contains(&(MemberId::from_raw_id(1), 124)) {
1737                    assert!(all_out.contains(&(MemberId::from_raw_id(1), 457)));
1738                    c_2_produced = true;
1739                }
1740
1741                if all_out.contains(&(MemberId::from_raw_id(0), 457))
1742                    && !all_out.contains(&(MemberId::from_raw_id(0), 124))
1743                {
1744                    c_1_saw_457_but_not_124 = true;
1745                }
1746            });
1747
1748        assert!(c_1_produced && c_2_produced); // in at least one execution each, the cluster member received both messages
1749
1750        // in at least one execution, the cluster member received 457 but not 124, this tests
1751        // that the simulator properly explores dynamic membership additions (a member that joins after 123 is broadcast)
1752        assert!(c_1_saw_457_but_not_124);
1753    }
1754
1755    #[cfg(feature = "sim")]
1756    #[test]
1757    fn sim_send_bincode_m2m() {
1758        let mut flow = FlowBuilder::new();
1759        let cluster = flow.cluster::<()>();
1760        let node = flow.process::<()>();
1761
1762        let input = node.source_iter(q!(vec![
1763            (MemberId::from_raw_id(0), 123),
1764            (MemberId::from_raw_id(1), 456),
1765        ]));
1766
1767        let out_recv = input
1768            .demux(&cluster, TCP.fail_stop().bincode())
1769            .map(q!(|x| x + 1))
1770            .flat_map_ordered(q!(|x| vec![
1771                (MemberId::from_raw_id(0), x),
1772                (MemberId::from_raw_id(1), x),
1773            ]))
1774            .demux(&cluster, TCP.fail_stop().bincode())
1775            .entries()
1776            .send(&node, TCP.fail_stop().bincode())
1777            .entries()
1778            .sim_output();
1779
1780        flow.sim()
1781            .with_cluster_size(&cluster, 4)
1782            .exhaustive(async || {
1783                out_recv
1784                    .assert_yields_only_unordered(vec![
1785                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 124)),
1786                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 457)),
1787                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 124)),
1788                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 457)),
1789                    ])
1790                    .await
1791            });
1792    }
1793
1794    #[cfg(feature = "sim")]
1795    #[test]
1796    fn sim_lossy_delayed_forever_o2o() {
1797        use std::collections::HashSet;
1798
1799        use crate::properties::manual_proof;
1800
1801        let mut flow = FlowBuilder::new();
1802        let node = flow.process::<()>();
1803        let node2 = flow.process::<()>();
1804
1805        let received = node
1806            .source_iter(q!(0..3_u32))
1807            .send(&node2, TCP.lossy_delayed_forever().bincode())
1808            .fold(
1809                q!(|| std::collections::HashSet::<u32>::new()),
1810                q!(
1811                    |set, v| {
1812                        set.insert(v);
1813                    },
1814                    commutative = manual_proof!(/** set insert is commutative */)
1815                ),
1816            );
1817
1818        let out_recv = sliced! {
1819            let snapshot = use::snapshot(received, nondet!(/** test */));
1820            snapshot.into_stream()
1821        }
1822        .sim_output();
1823
1824        let mut saw_non_contiguous = false;
1825
1826        flow.sim().test_safety_only().exhaustive(async || {
1827            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1828
1829            // Check each individual snapshot for a non-contiguous subset.
1830            for set in &snapshots {
1831                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1832                if set.len() >= 2 && set.len() < 3 {
1833                    let min = *set.iter().min().unwrap();
1834                    let max = *set.iter().max().unwrap();
1835                    if set.len() < (max - min + 1) as usize {
1836                        saw_non_contiguous = true;
1837                    }
1838                }
1839            }
1840        });
1841
1842        assert!(
1843            saw_non_contiguous,
1844            "Expected at least one execution with a non-contiguous subset of inputs"
1845        );
1846    }
1847
1848    #[cfg(feature = "sim")]
1849    #[test]
1850    fn sim_udp_lossy_delayed_forever_o2o() {
1851        use std::collections::HashSet;
1852
1853        use crate::networking::UDP;
1854        use crate::properties::manual_proof;
1855
1856        let mut flow = FlowBuilder::new();
1857        let node = flow.process::<()>();
1858        let node2 = flow.process::<()>();
1859
1860        let received = node
1861            .source_iter(q!(0..3_u32))
1862            .send(&node2, UDP.lossy_delayed_forever().bincode())
1863            .fold(
1864                q!(|| std::collections::HashSet::<u32>::new()),
1865                q!(
1866                    |set, v| {
1867                        set.insert(v);
1868                    },
1869                    commutative = manual_proof!(/** set insert is commutative */)
1870                ),
1871            );
1872
1873        let out_recv = sliced! {
1874            let snapshot = use::snapshot(received, nondet!(/** test */));
1875            snapshot.into_stream()
1876        }
1877        .sim_output();
1878
1879        let mut saw_non_contiguous = false;
1880
1881        flow.sim().test_safety_only().exhaustive(async || {
1882            let snapshots = out_recv.collect::<Vec<HashSet<u32>>>().await;
1883
1884            // Check each individual snapshot for a non-contiguous subset.
1885            for set in &snapshots {
1886                #[expect(clippy::disallowed_methods, reason = "min / max are deterministic")]
1887                if set.len() >= 2 && set.len() < 3 {
1888                    let min = *set.iter().min().unwrap();
1889                    let max = *set.iter().max().unwrap();
1890                    if set.len() < (max - min + 1) as usize {
1891                        saw_non_contiguous = true;
1892                    }
1893                }
1894            }
1895        });
1896
1897        assert!(
1898            saw_non_contiguous,
1899            "Expected at least one execution with a non-contiguous subset of inputs"
1900        );
1901    }
1902
1903    #[cfg(feature = "sim")]
1904    #[test]
1905    fn sim_broadcast_closed_o2m() {
1906        let mut flow = FlowBuilder::new();
1907        let cluster = flow.cluster::<()>();
1908        let node = flow.process::<()>();
1909
1910        let input = node.source_iter(q!(vec![123, 456]));
1911
1912        let out_recv = input
1913            .broadcast_closed(&cluster, TCP.fail_stop().bincode())
1914            .send(&node, TCP.fail_stop().bincode())
1915            .entries()
1916            .sim_output();
1917
1918        flow.sim()
1919            .with_cluster_size(&cluster, 2)
1920            .exhaustive(async || {
1921                out_recv
1922                    .assert_yields_only_unordered(vec![
1923                        (MemberId::from_raw_id(0), 123),
1924                        (MemberId::from_raw_id(0), 456),
1925                        (MemberId::from_raw_id(1), 123),
1926                        (MemberId::from_raw_id(1), 456),
1927                    ])
1928                    .await
1929            });
1930    }
1931
1932    #[cfg(feature = "sim")]
1933    #[test]
1934    fn sim_broadcast_closed_m2m() {
1935        let mut flow = FlowBuilder::new();
1936        let source = flow.cluster::<()>();
1937        let dest: crate::location::Cluster<'_, ()> = flow.cluster::<()>();
1938        let node = flow.process::<()>();
1939
1940        let input = source.source_iter(q!(vec![123]));
1941
1942        // Broadcast from source cluster to dest cluster, then collect at a process.
1943        let out_recv = input
1944            .broadcast_closed(&dest, TCP.fail_stop().bincode())
1945            .entries()
1946            .send(&node, TCP.fail_stop().bincode())
1947            .entries()
1948            .sim_output();
1949
1950        flow.sim()
1951            .with_cluster_size(&source, 2)
1952            .with_cluster_size(&dest, 2)
1953            .exhaustive(async || {
1954                // Each source member (0, 1) broadcasts 123 to each dest member (0, 1).
1955                // The dest members then send to the process keyed by dest member id.
1956                // Each dest member receives (source_0, 123) and (source_1, 123).
1957                out_recv
1958                    .assert_yields_only_unordered(vec![
1959                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(0), 123)),
1960                        (MemberId::from_raw_id(0), (MemberId::from_raw_id(1), 123)),
1961                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(0), 123)),
1962                        (MemberId::from_raw_id(1), (MemberId::from_raw_id(1), 123)),
1963                    ])
1964                    .await
1965            });
1966    }
1967
1968    /// Compile-time check that the consistency guarantee of `broadcast_closed` output tracks
1969    /// the network's failure policy: `fail_stop` and `lossy_delayed_forever` preserve
1970    /// [`EventualConsistency`], while plain `lossy` only provides [`NoConsistency`].
1971    #[cfg(feature = "sim")]
1972    #[test]
1973    fn broadcast_closed_consistency_tracks_failure_policy() {
1974        use crate::live_collections::keyed_stream::KeyedStream;
1975        use crate::live_collections::stream::Stream;
1976        use crate::location::Cluster;
1977        use crate::location::cluster::{EventualConsistency, NoConsistency};
1978
1979        let mut flow = FlowBuilder::new();
1980        let cluster = flow.cluster::<()>();
1981        let source = flow.cluster::<()>();
1982        let node = flow.process::<()>();
1983
1984        // `fail_stop` models a failed connection as the recipient having failed, preserving
1985        // eventual consistency across live members.
1986        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
1987            .source_iter(q!(vec![1u32]))
1988            .broadcast_closed(&cluster, TCP.fail_stop().bincode());
1989
1990        // `lossy_delayed_forever` models drops as indefinite delays, preserving eventual
1991        // consistency.
1992        let _: Stream<u32, Cluster<'_, (), EventualConsistency>, _, _, _> = node
1993            .source_iter(q!(vec![1u32]))
1994            .broadcast_closed(&cluster, TCP.lossy_delayed_forever().bincode());
1995
1996        // Plain `lossy` can drop messages for some members while delivering them to others,
1997        // so replicas may permanently diverge.
1998        let _: Stream<u32, Cluster<'_, (), NoConsistency>, _, _, _> = node
1999            .source_iter(q!(vec![1u32]))
2000            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
2001
2002        // The same applies to cluster-to-cluster closed broadcasts.
2003        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), EventualConsistency>, _, _, _> =
2004            source
2005                .source_iter(q!(vec![1u32]))
2006                .broadcast_closed(&cluster, TCP.fail_stop().bincode());
2007
2008        let _: KeyedStream<MemberId<()>, u32, Cluster<'_, (), NoConsistency>, _, _, _> = source
2009            .source_iter(q!(vec![1u32]))
2010            .broadcast_closed(&cluster, TCP.lossy(nondet!(/** test */)).bincode());
2011
2012        let _ = flow.finalize();
2013    }
2014}