Skip to main content

hydro_lang/location/
mod.rs

1//! Type definitions for distributed locations, which specify where pieces of a Hydro
2//! program will be executed.
3//!
4//! Hydro is a **global**, **distributed** programming model. This means that the data
5//! and computation in a Hydro program can be spread across multiple machines, data
6//! centers, and even continents. To achieve this, Hydro uses the concept of
7//! **locations** to keep track of _where_ data is located and computation is executed.
8//!
9//! Each live collection type (in [`crate::live_collections`]) has a type parameter `L`
10//! which will always be a type that implements the [`Location`] trait (e.g. [`Process`]
11//! and [`Cluster`]). To create distributed programs, Hydro provides a variety of APIs
12//! to allow live collections to be _moved_ between locations via network send/receive.
13//!
14//! See [the Hydro docs](https://hydro.run/docs/hydro/reference/locations/) for more information.
15
16use std::fmt::Debug;
17use std::future::Future;
18#[cfg(feature = "tokio")]
19use std::marker::PhantomData;
20use std::num::ParseIntError;
21#[cfg(feature = "tokio")]
22use std::time::Duration;
23
24#[cfg(feature = "tokio")]
25use bytes::{Bytes, BytesMut};
26use futures::stream::Stream as FuturesStream;
27use proc_macro2::Span;
28use quote::quote;
29#[cfg(feature = "tokio")]
30use serde::de::DeserializeOwned;
31use serde::{Deserialize, Serialize};
32use slotmap::{Key, new_key_type};
33#[cfg(feature = "tokio")]
34use stageleft::quote_type;
35use stageleft::runtime_support::{FreeVariableWithContextWithProps, QuoteTokens};
36use stageleft::{QuotedWithContext, q};
37use syn::parse_quote;
38#[cfg(feature = "tokio")]
39use tokio_util::codec::{Decoder, Encoder, LengthDelimitedCodec};
40
41#[cfg(feature = "tokio")]
42use crate::compile::builder::ExternalPortId;
43#[cfg(feature = "tokio")]
44use crate::compile::ir::DebugInstantiate;
45use crate::compile::ir::{
46    ClusterMembersState, HydroIrOpMetadata, HydroNode, HydroRoot, HydroSource,
47};
48use crate::forward_handle::ForwardRef;
49#[cfg(stageleft_runtime)]
50use crate::forward_handle::{CycleCollection, ForwardHandle};
51use crate::live_collections::boundedness::{Bounded, Unbounded};
52use crate::live_collections::keyed_stream::KeyedStream;
53use crate::live_collections::singleton::Singleton;
54#[cfg(feature = "sim")]
55#[cfg(stageleft_runtime)]
56use crate::live_collections::stream::networking::serialize_bincode;
57use crate::live_collections::stream::{ExactlyOnce, NoOrder, Stream, TotalOrder};
58#[cfg(feature = "tokio")]
59use crate::live_collections::stream::{Ordering, Retries};
60#[cfg(stageleft_runtime)]
61use crate::location::dynamic::DynLocation;
62use crate::location::dynamic::{ClusterConsistency, LocationId};
63#[cfg(feature = "tokio")]
64use crate::location::external_process::{
65    ExternalBincodeBidi, ExternalBincodeSink, ExternalBytesPort, Many, NotMany,
66};
67use crate::nondet::NonDet;
68#[cfg(feature = "tokio")]
69use crate::properties::manual_proof;
70#[cfg(feature = "sim")]
71use crate::sim::SimSender;
72use crate::staging_util::get_this_crate;
73
74pub mod dynamic;
75
76pub mod external_process;
77pub use external_process::External;
78
79pub mod process;
80pub use process::Process;
81
82pub mod cluster;
83pub use cluster::Cluster;
84
85pub mod member_id;
86pub use member_id::{MemberId, TaglessMemberId};
87
88pub mod tick;
89pub use tick::{Atomic, Tick};
90
91/// An event indicating a change in membership status of a location in a group
92/// (e.g. a node in a [`Cluster`] or an external client connection).
93#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
94pub enum MembershipEvent {
95    /// The member has joined the group and is now active.
96    Joined,
97    /// The member has left the group and is no longer active.
98    Left,
99}
100
101/// A hint for configuring the network transport used by an external connection.
102///
103/// This controls how the underlying TCP listener is set up when binding
104/// external client connections via methods like [`Location::bind_single_client`]
105/// or [`Location::bidi_external_many_bytes`].
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum NetworkHint {
108    /// Automatically select the network configuration (e.g. an ephemeral port).
109    Auto,
110    /// Use a TCP port, optionally specifying a fixed port number.
111    ///
112    /// If `None`, an available port will be chosen automatically.
113    /// If `Some(port)`, the given port number will be used.
114    TcpPort(Option<u16>),
115}
116
117#[track_caller]
118pub(crate) fn check_matching_location<'a, L: Location<'a>>(l1: &L, l2: &L) {
119    assert_eq!(Location::id(l1), Location::id(l2), "locations do not match");
120}
121
122#[stageleft::export(LocationKey)]
123new_key_type! {
124    /// A unique identifier for a clock tick.
125    pub struct LocationKey;
126}
127
128impl std::fmt::Display for LocationKey {
129    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130        write!(f, "loc{:?}", self.data()) // `"loc1v1"``
131    }
132}
133
134/// This is used for the ECS membership stream.
135/// TODO(mingwei): Make this more robust?
136impl std::str::FromStr for LocationKey {
137    type Err = Option<ParseIntError>;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        let nvn = s.strip_prefix("loc").ok_or(None)?;
141        let (idx, ver) = nvn.split_once("v").ok_or(None)?;
142        let idx: u64 = idx.parse()?;
143        let ver: u64 = ver.parse()?;
144        Ok(slotmap::KeyData::from_ffi((ver << 32) | idx).into())
145    }
146}
147
148impl LocationKey {
149    /// TODO(minwgei): Remove this and avoid magic key for simulator external.
150    /// The first location key, used by the simulator as the default external location.
151    pub const FIRST: Self = Self(slotmap::KeyData::from_ffi(0x0000000100000001)); // `1v1`
152
153    /// A key for testing with index 1.
154    #[cfg(test)]
155    pub const TEST_KEY_1: Self = Self(slotmap::KeyData::from_ffi(0x000000FF00000001)); // `1v255`
156
157    /// A key for testing with index 2.
158    #[cfg(test)]
159    pub const TEST_KEY_2: Self = Self(slotmap::KeyData::from_ffi(0x000000FF00000002)); // `2v255`
160}
161
162/// This is used within `q!` code in docker and ECS.
163impl<Ctx> FreeVariableWithContextWithProps<Ctx, ()> for LocationKey {
164    type O = LocationKey;
165
166    fn to_tokens(self, _ctx: &Ctx) -> (QuoteTokens, ())
167    where
168        Self: Sized,
169    {
170        let root = get_this_crate();
171        let n = Key::data(&self).as_ffi();
172        (
173            QuoteTokens {
174                prelude: None,
175                expr: Some(quote! {
176                    #root::location::LocationKey::from(#root::runtime_support::slotmap::KeyData::from_ffi(#n))
177                }),
178            },
179            (),
180        )
181    }
182}
183
184/// A simple enum for the type of a root location.
185#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
186pub enum LocationType {
187    /// A process (single node).
188    Process,
189    /// A cluster (multiple nodes).
190    Cluster,
191    /// An external client.
192    External,
193}
194
195/// A top-level location (i.e. a [`Process`] or [`Cluster`]) that is outside a tick / atomic region.
196pub trait TopLevel<'a>: Location<'a> {}
197
198#[cfg(feature = "sim")]
199#[cfg(stageleft_runtime)]
200fn register_serialized_external_input<'a, At, L, T>(
201    at: &At,
202    from: &External<'_, L>,
203    deserialize_fn: syn::Expr,
204) -> (
205    ExternalPortId,
206    Stream<T, At::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
207)
208where
209    At: TopLevel<'a> + Sized,
210{
211    let (port_id, stream, sink) = at.register_serialized_single_client::<_, T, ()>(
212        from,
213        serialize_bincode::<()>(false),
214        deserialize_fn,
215    );
216    sink.complete(stream.location().source_iter(q!([])));
217
218    (port_id, stream)
219}
220
221/// A location where data can be materialized and computation can be executed.
222///
223/// Hydro is a **global**, **distributed** programming model. This means that the data
224/// and computation in a Hydro program can be spread across multiple machines, data
225/// centers, and even continents. To achieve this, Hydro uses the concept of
226/// **locations** to keep track of _where_ data is located and computation is executed.
227///
228/// Each live collection type (in [`crate::live_collections`]) has a type parameter `L`
229/// which will always be a type that implements the [`Location`] trait (e.g. [`Process`]
230/// and [`Cluster`]). To create distributed programs, Hydro provides a variety of APIs
231/// to allow live collections to be _moved_ between locations via network send/receive.
232///
233/// See [the Hydro docs](https://hydro.run/docs/hydro/reference/locations/) for more information.
234#[expect(
235    private_bounds,
236    reason = "only internal Hydro code can define location types"
237)]
238pub trait Location<'a>: DynLocation {
239    /// The root location type for this location.
240    ///
241    /// For top-level locations like [`Process`] and [`Cluster`], this is `Self`.
242    /// For nested locations like [`Tick`], this is the root location that contains it.
243    type Root: Location<'a>;
244
245    /// Location type with consistency guarantees dropped for the live collection on it.
246    type DropConsistency: Location<'a, DropConsistency = Self::DropConsistency>;
247
248    /// Returns the root location for this location.
249    ///
250    /// For top-level locations like [`Process`] and [`Cluster`], this returns `self`.
251    /// For nested locations like [`Tick`], this returns the root location that contains it.
252    fn root(&self) -> Self::Root;
253
254    /// This location but with consistency guarantees dropped for the live collection
255    fn drop_consistency(&self) -> Self::DropConsistency;
256    /// Gets the runtime enum variant for the current consistency level, if this is a cluster.
257    fn consistency() -> Option<ClusterConsistency>;
258
259    /// Updates the consistency guarantees to match that of the given location.
260    fn with_consistency_of<L2: Location<'a, DropConsistency = Self::DropConsistency>>(&self) -> L2 {
261        L2::from_drop_consistency(self.drop_consistency())
262    }
263
264    #[doc(hidden)]
265    fn from_drop_consistency(l2: Self::DropConsistency) -> Self;
266
267    /// Attempts to create a new [`Tick`] clock domain at this location.
268    ///
269    /// Returns `Some(Tick)` if this is a top-level location (like [`Process`] or [`Cluster`]),
270    /// or `None` if this location is already inside a tick (nested ticks are not supported).
271    ///
272    /// Prefer using [`Location::tick`] when you know the location is top-level.
273    fn try_tick(&self) -> Option<Tick<Self>> {
274        if Self::is_top_level() {
275            let id = if let LocationId::Atomic { .. } = self.id() {
276                None
277            } else {
278                Some(self.flow_state().borrow_mut().next_clock_id())
279            };
280            Some(Tick {
281                id,
282                l: self.clone(),
283            })
284        } else {
285            None
286        }
287    }
288
289    /// Returns the unique identifier for this location.
290    fn id(&self) -> LocationId {
291        DynLocation::dyn_id(self)
292    }
293
294    /// Creates a new [`Tick`] clock domain at this location.
295    ///
296    /// A tick represents a logical clock that can be used to batch streaming data
297    /// into discrete time steps. This is useful for implementing iterative algorithms
298    /// or for synchronizing data across multiple streams.
299    ///
300    /// # Example
301    /// ```rust
302    /// # #[cfg(feature = "deploy")] {
303    /// # use hydro_lang::prelude::*;
304    /// # use futures::StreamExt;
305    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
306    /// let tick = process.tick();
307    /// let inside_tick = process
308    ///     .source_iter(q!(vec![1, 2, 3, 4]))
309    ///     .batch(&tick, nondet!(/** test */));
310    /// inside_tick.all_ticks()
311    /// # }, |mut stream| async move {
312    /// // 1, 2, 3, 4
313    /// # for w in vec![1, 2, 3, 4] {
314    /// #     assert_eq!(stream.next().await.unwrap(), w);
315    /// # }
316    /// # }));
317    /// # }
318    /// ```
319    fn tick(&self) -> Tick<Self> {
320        self.try_tick().expect("cannot create nested ticks")
321    }
322
323    /// Creates an unbounded stream that continuously emits unit values `()`.
324    ///
325    /// This is useful for driving computations that need to run continuously,
326    /// such as polling or heartbeat mechanisms.
327    ///
328    /// # Example
329    /// ```rust
330    /// # #[cfg(feature = "deploy")] {
331    /// # use hydro_lang::prelude::*;
332    /// # use futures::StreamExt;
333    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
334    /// let tick = process.tick();
335    /// process.spin()
336    ///     .batch(&tick, nondet!(/** test */))
337    ///     .map(q!(|_| 42))
338    ///     .all_ticks()
339    /// # }, |mut stream| async move {
340    /// // 42, 42, 42, ...
341    /// # assert_eq!(stream.next().await.unwrap(), 42);
342    /// # assert_eq!(stream.next().await.unwrap(), 42);
343    /// # assert_eq!(stream.next().await.unwrap(), 42);
344    /// # }));
345    /// # }
346    /// ```
347    fn spin(&self) -> Stream<(), Self, Unbounded, TotalOrder, ExactlyOnce>
348    where
349        Self: TopLevel<'a> + Sized,
350    {
351        Stream::new(
352            self.clone(),
353            HydroNode::Source {
354                source: HydroSource::Spin(),
355                metadata: self.new_node_metadata(Stream::<
356                    (),
357                    Self,
358                    Unbounded,
359                    TotalOrder,
360                    ExactlyOnce,
361                >::collection_kind()),
362            },
363        )
364    }
365
366    /// Creates a stream from an async [`FuturesStream`].
367    ///
368    /// This is useful for integrating with external async data sources,
369    /// such as network connections or file readers.
370    ///
371    /// # Example
372    /// ```rust
373    /// # #[cfg(feature = "deploy")] {
374    /// # use hydro_lang::prelude::*;
375    /// # use futures::StreamExt;
376    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
377    /// process.source_stream(q!(futures::stream::iter(vec![1, 2, 3])))
378    /// # }, |mut stream| async move {
379    /// // 1, 2, 3
380    /// # for w in vec![1, 2, 3] {
381    /// #     assert_eq!(stream.next().await.unwrap(), w);
382    /// # }
383    /// # }));
384    /// # }
385    /// ```
386    fn source_stream<T, E>(
387        &self,
388        e: impl QuotedWithContext<'a, E, Self>,
389    ) -> Stream<T, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>
390    where
391        E: FuturesStream<Item = T> + Unpin,
392        Self: TopLevel<'a> + Sized,
393    {
394        let e = e.splice_untyped_ctx(self);
395
396        let target_location = self.drop_consistency();
397        Stream::new(
398            target_location.clone(),
399            HydroNode::Source {
400                source: HydroSource::Stream(e.into()),
401                metadata: target_location.new_node_metadata(Stream::<
402                    T,
403                    Self::DropConsistency,
404                    Unbounded,
405                    TotalOrder,
406                    ExactlyOnce,
407                >::collection_kind()),
408            },
409        )
410    }
411
412    /// Creates a bounded stream from an iterator.
413    ///
414    /// The iterator is evaluated once at runtime, and all elements are emitted
415    /// in order. This is useful for creating streams from static data or
416    /// for testing.
417    ///
418    /// # Example
419    /// ```rust
420    /// # #[cfg(feature = "deploy")] {
421    /// # use hydro_lang::prelude::*;
422    /// # use futures::StreamExt;
423    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
424    /// process.source_iter(q!(vec![1, 2, 3, 4]))
425    /// # }, |mut stream| async move {
426    /// // 1, 2, 3, 4
427    /// # for w in vec![1, 2, 3, 4] {
428    /// #     assert_eq!(stream.next().await.unwrap(), w);
429    /// # }
430    /// # }));
431    /// # }
432    /// ```
433    fn source_iter<T, E>(
434        &self,
435        e: impl QuotedWithContext<'a, E, Self>,
436    ) -> Stream<T, Self::DropConsistency, Bounded, TotalOrder, ExactlyOnce>
437    where
438        E: IntoIterator<Item = T>,
439        Self: Sized,
440    {
441        let e = e.splice_typed_ctx(self);
442
443        let target_location = self.drop_consistency();
444        Stream::new(
445            target_location.clone(),
446            HydroNode::Source {
447                source: HydroSource::Iter(e.into()),
448                metadata: target_location.new_node_metadata(Stream::<
449                    T,
450                    Self::DropConsistency,
451                    Bounded,
452                    TotalOrder,
453                    ExactlyOnce,
454                >::collection_kind()),
455            },
456        )
457    }
458
459    #[deprecated(note = "use .source_cluster_membership_stream(...) instead")]
460    /// Creates a stream of membership events for a cluster.
461    ///
462    /// This stream emits [`MembershipEvent::Joined`] when a cluster member joins
463    /// and [`MembershipEvent::Left`] when a cluster member leaves. The stream is
464    /// keyed by the [`MemberId`] of the cluster member.
465    ///
466    /// This is useful for implementing protocols that need to track cluster membership,
467    /// such as broadcasting to all members or detecting failures.
468    ///
469    /// # Non-Determinism
470    /// This stream is non-deterministic because the timing of membership events, for example
471    /// if a node leaves, the membership event may not be received if the node left before the
472    /// stream was created.
473    ///
474    /// # Example
475    /// ```rust
476    /// # #[cfg(feature = "deploy")] {
477    /// # use hydro_lang::prelude::*;
478    /// # use futures::StreamExt;
479    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
480    /// let p1 = flow.process::<()>();
481    /// let workers: Cluster<()> = flow.cluster::<()>();
482    /// # // do nothing on each worker
483    /// # workers.source_iter(q!(vec![])).for_each(q!(|_: ()| {}));
484    /// let cluster_members = p1.source_cluster_members(&workers, nondet!(/** late joiners may miss events */));
485    /// # cluster_members.entries().send(&p2, TCP.fail_stop().bincode())
486    /// // if there are 4 members in the cluster, we would see a join event for each
487    /// // { MemberId::<Worker>(0): [MembershipEvent::Join], MemberId::<Worker>(2): [MembershipEvent::Join], ... }
488    /// # }, |mut stream| async move {
489    /// # let mut results = Vec::new();
490    /// # for w in 0..4 {
491    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
492    /// # }
493    /// # results.sort();
494    /// # assert_eq!(results, vec!["(MemberId::<()>(0), Joined)", "(MemberId::<()>(1), Joined)", "(MemberId::<()>(2), Joined)", "(MemberId::<()>(3), Joined)"]);
495    /// # }));
496    /// # }
497    /// ```
498    fn source_cluster_members<C: 'a>(
499        &self,
500        cluster: &Cluster<'a, C>,
501        nondet_start: NonDet,
502    ) -> KeyedStream<MemberId<C>, MembershipEvent, Self::DropConsistency, Unbounded>
503    where
504        Self: TopLevel<'a> + Sized,
505    {
506        self.source_cluster_membership_stream(cluster, nondet_start)
507    }
508
509    /// Creates a stream of membership events for a cluster.
510    ///
511    /// This stream emits [`MembershipEvent::Joined`] when a cluster member joins
512    /// and [`MembershipEvent::Left`] when a cluster member leaves. The stream is
513    /// keyed by the [`MemberId`] of the cluster member.
514    ///
515    /// This is useful for implementing protocols that need to track cluster membership,
516    /// such as broadcasting to all members or detecting failures.
517    ///
518    /// # Non-Determinism
519    /// This stream is non-deterministic because the timing of membership events, for example
520    /// if a node leaves, the membership event may not be received if the node left before the
521    /// stream was created.
522    ///
523    /// # Example
524    /// ```rust
525    /// # #[cfg(feature = "deploy")] {
526    /// # use hydro_lang::prelude::*;
527    /// # use futures::StreamExt;
528    /// # tokio_test::block_on(hydro_lang::test_util::multi_location_test(|flow, p2| {
529    /// let p1 = flow.process::<()>();
530    /// let workers: Cluster<()> = flow.cluster::<()>();
531    /// # // do nothing on each worker
532    /// # workers.source_iter(q!(vec![])).for_each(q!(|_: ()| {}));
533    /// let cluster_members = p1.source_cluster_membership_stream(&workers, nondet!(/** late joiners may miss events */));
534    /// # cluster_members.entries().send(&p2, TCP.fail_stop().bincode())
535    /// // if there are 4 members in the cluster, we would see a join event for each
536    /// // { MemberId::<Worker>(0): [MembershipEvent::Join], MemberId::<Worker>(2): [MembershipEvent::Join], ... }
537    /// # }, |mut stream| async move {
538    /// # let mut results = Vec::new();
539    /// # for w in 0..4 {
540    /// #     results.push(format!("{:?}", stream.next().await.unwrap()));
541    /// # }
542    /// # results.sort();
543    /// # assert_eq!(results, vec!["(MemberId::<()>(0), Joined)", "(MemberId::<()>(1), Joined)", "(MemberId::<()>(2), Joined)", "(MemberId::<()>(3), Joined)"]);
544    /// # }));
545    /// # }
546    /// ```
547    fn source_cluster_membership_stream<C: 'a>(
548        &self,
549        cluster: &Cluster<'a, C>,
550        _nondet_start: NonDet,
551    ) -> KeyedStream<MemberId<C>, MembershipEvent, Self::DropConsistency, Unbounded>
552    where
553        Self: TopLevel<'a> + Sized,
554    {
555        let target_consistency = self.drop_consistency();
556        Stream::new(
557            target_consistency.clone(),
558            HydroNode::Source {
559                source: HydroSource::ClusterMembers(cluster.id(), ClusterMembersState::Uninit),
560                metadata: target_consistency.new_node_metadata(Stream::<
561                    (TaglessMemberId, MembershipEvent),
562                    Self,
563                    Unbounded,
564                    TotalOrder,
565                    ExactlyOnce,
566                >::collection_kind(
567                )),
568            },
569        )
570        .map(q!(|(k, v)| (MemberId::from_tagless(k), v)))
571        .into_keyed()
572    }
573
574    /// Creates a one-way connection from an external process to receive raw bytes.
575    ///
576    /// Returns a port handle for the external process to connect to, and a stream
577    /// of received byte buffers.
578    ///
579    /// For bidirectional communication or typed data, see [`Location::bind_single_client`]
580    /// or [`Location::source_external_bincode`].
581    #[cfg(feature = "tokio")]
582    fn source_external_bytes<L>(
583        &self,
584        from: &External<'_, L>,
585    ) -> (
586        ExternalBytesPort,
587        Stream<BytesMut, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
588    )
589    where
590        Self: TopLevel<'a> + Sized,
591    {
592        let (port, stream, sink) =
593            self.bind_single_client::<_, Bytes, LengthDelimitedCodec>(from, NetworkHint::Auto);
594
595        sink.complete(stream.location().source_iter(q!([])));
596
597        (port, stream)
598    }
599
600    /// Creates a one-way connection from an external process to receive bincode-serialized data.
601    ///
602    /// Returns a sink handle for the external process to send data to, and a stream
603    /// of received values.
604    ///
605    /// For bidirectional communication, see [`Location::bind_single_client_bincode`].
606    #[cfg(feature = "tokio")]
607    fn source_external_bincode<L, T, O: Ordering, R: Retries>(
608        &self,
609        from: &External<'_, L>,
610    ) -> (
611        ExternalBincodeSink<T, NotMany, O, R>,
612        Stream<T, Self::DropConsistency, Unbounded, O, R>,
613    )
614    where
615        Self: TopLevel<'a> + Sized,
616        T: Serialize + DeserializeOwned,
617    {
618        let (port, stream, sink) = self.bind_single_client_bincode::<_, T, ()>(from);
619        sink.complete(stream.location().source_iter(q!([])));
620
621        (
622            ExternalBincodeSink {
623                process_key: from.key,
624                port_id: port.port_id,
625                _phantom: PhantomData,
626            },
627            stream.weaken_ordering().weaken_retries(),
628        )
629    }
630
631    /// Sets up a bincode-encoded simulated input port on this location for testing.
632    ///
633    /// Returns a handle to send messages to the location as well as a stream
634    /// of received messages. Use [`Location::sim_input_with`] to select another codec.
635    /// This is only available when the `sim` feature is enabled.
636    #[cfg(feature = "sim")]
637    fn sim_input<T, O: Ordering, R: Retries>(
638        &self,
639    ) -> (
640        SimSender<T, O, R>,
641        Stream<T, Self::DropConsistency, Unbounded, O, R>,
642    )
643    where
644        Self: TopLevel<'a> + Sized,
645        T: Serialize + DeserializeOwned,
646    {
647        self.sim_input_with(crate::sim::codec::BincodeCodec)
648    }
649
650    /// Sets up a simulated input port using `codec`.
651    ///
652    /// Returns a handle to send messages to the location as well as a stream
653    /// of received messages. Custom codecs implement
654    /// [`SimCodec`](crate::sim::codec::SimCodec), which documents where they must be defined.
655    /// This is only available when the `sim` feature is enabled.
656    #[cfg(feature = "sim")]
657    fn sim_input_with<T, O: Ordering, R: Retries, C: crate::sim::codec::SimCodec<T>>(
658        &self,
659        _codec: C,
660    ) -> (
661        SimSender<T, O, R>,
662        Stream<T, Self::DropConsistency, Unbounded, O, R>,
663    )
664    where
665        Self: TopLevel<'a> + Sized,
666    {
667        let external_location: External<'a, ()> = External {
668            key: LocationKey::FIRST,
669            flow_state: self.flow_state().clone(),
670            _phantom: PhantomData,
671        };
672
673        let (external_port_id, stream) = register_serialized_external_input(
674            self,
675            &external_location,
676            crate::sim::codec::staged_deserialize::<T, C>(),
677        );
678
679        (
680            SimSender(external_port_id, PhantomData, C::encode),
681            stream.weaken_ordering().weaken_retries(),
682        )
683    }
684
685    /// Creates an external input stream for embedded deployment mode.
686    ///
687    /// The `name` parameter specifies the name of the generated function parameter
688    /// that will supply data to this stream at runtime. The generated function will
689    /// accept an `impl Stream<Item = T> + Unpin` argument with this name.
690    fn embedded_input<T>(
691        &self,
692        name: impl Into<String>,
693    ) -> Stream<T, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>
694    where
695        Self: TopLevel<'a> + Sized,
696    {
697        let ident = syn::Ident::new(&name.into(), Span::call_site());
698
699        let target_location = self.drop_consistency();
700        Stream::new(
701            target_location.clone(),
702            HydroNode::Source {
703                source: HydroSource::Embedded(ident),
704                metadata: target_location.new_node_metadata(Stream::<
705                    T,
706                    Self,
707                    Unbounded,
708                    TotalOrder,
709                    ExactlyOnce,
710                >::collection_kind()),
711            },
712        )
713    }
714
715    /// Creates an embedded singleton input for embedded deployment mode.
716    ///
717    /// The `name` parameter specifies the name of the generated function parameter
718    /// that will supply data to this singleton at runtime. The generated function will
719    /// accept a plain `T` parameter with this name.
720    fn embedded_singleton_input<T>(
721        &self,
722        name: impl Into<String>,
723    ) -> Singleton<T, Self::DropConsistency, Bounded>
724    where
725        Self: TopLevel<'a> + Sized,
726    {
727        let ident = syn::Ident::new(&name.into(), Span::call_site());
728
729        let target_location = self.drop_consistency();
730        Singleton::new(
731            target_location.clone(),
732            HydroNode::Source {
733                source: HydroSource::EmbeddedSingleton(ident),
734                metadata: target_location
735                    .new_node_metadata(Singleton::<T, Self, Bounded>::collection_kind()),
736            },
737        )
738    }
739
740    /// Establishes a server on this location to receive a bidirectional connection from a single
741    /// client, identified by the given `External` handle. Returns a port handle for the external
742    /// process to connect to, a stream of incoming messages, and a handle to send outgoing
743    /// messages.
744    ///
745    /// # Example
746    /// ```rust
747    /// # #[cfg(feature = "deploy")] {
748    /// # use hydro_lang::prelude::*;
749    /// # use hydro_deploy::Deployment;
750    /// # use futures::{SinkExt, StreamExt};
751    /// # tokio_test::block_on(async {
752    /// # use bytes::Bytes;
753    /// # use hydro_lang::location::NetworkHint;
754    /// # use tokio_util::codec::LengthDelimitedCodec;
755    /// # let mut flow = FlowBuilder::new();
756    /// let node = flow.process::<()>();
757    /// let external = flow.external::<()>();
758    /// let (port, incoming, outgoing) =
759    ///     node.bind_single_client::<_, Bytes, LengthDelimitedCodec>(&external, NetworkHint::Auto);
760    /// outgoing.complete(incoming.map(q!(|data /* : Bytes */| {
761    ///     let mut resp: Vec<u8> = data.into();
762    ///     resp.push(42);
763    ///     resp.into() // : Bytes
764    /// })));
765    ///
766    /// # let mut deployment = Deployment::new();
767    /// let nodes = flow // ... with_process and with_external
768    /// #     .with_process(&node, deployment.Localhost())
769    /// #     .with_external(&external, deployment.Localhost())
770    /// #     .deploy(&mut deployment);
771    ///
772    /// deployment.deploy().await.unwrap();
773    /// deployment.start().await.unwrap();
774    ///
775    /// let (mut external_out, mut external_in) = nodes.connect(port).await;
776    /// external_in.send(vec![1, 2, 3].into()).await.unwrap();
777    /// assert_eq!(
778    ///     external_out.next().await.unwrap().unwrap(),
779    ///     vec![1, 2, 3, 42]
780    /// );
781    /// # });
782    /// # }
783    /// ```
784    #[cfg(feature = "tokio")]
785    #[expect(clippy::type_complexity, reason = "stream markers")]
786    fn bind_single_client<L, T, Codec: Encoder<T> + Decoder>(
787        &self,
788        from: &External<'_, L>,
789        port_hint: NetworkHint,
790    ) -> (
791        ExternalBytesPort<NotMany>,
792        Stream<<Codec as Decoder>::Item, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
793        ForwardHandle<'a, Stream<T, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>>,
794    )
795    where
796        Self: TopLevel<'a> + Sized,
797    {
798        let next_external_port_id = from.flow_state.borrow_mut().next_external_port();
799        let target_consistency = self.drop_consistency();
800
801        let (fwd_ref, to_sink) = target_consistency.forward_ref::<Stream<
802            T,
803            Self::DropConsistency,
804            Unbounded,
805            TotalOrder,
806            ExactlyOnce,
807        >>();
808        let mut flow_state_borrow = self.flow_state().borrow_mut();
809
810        flow_state_borrow.push_root(HydroRoot::SendExternal {
811            to_external_key: from.key,
812            to_port_id: next_external_port_id,
813            to_many: false,
814            unpaired: false,
815            serialize_fn: None,
816            instantiate_fn: DebugInstantiate::Building,
817            input: Box::new(to_sink.ir_node.replace(HydroNode::Placeholder)),
818            op_metadata: HydroIrOpMetadata::new(),
819        });
820        drop(flow_state_borrow);
821
822        let raw_stream: Stream<
823            Result<<Codec as Decoder>::Item, <Codec as Decoder>::Error>,
824            Self::DropConsistency,
825            Unbounded,
826            TotalOrder,
827            ExactlyOnce,
828        > = Stream::new(
829            target_consistency.clone(),
830            HydroNode::ExternalInput {
831                from_external_key: from.key,
832                from_port_id: next_external_port_id,
833                from_many: false,
834                codec_type: quote_type::<Codec>().into(),
835                port_hint,
836                instantiate_fn: DebugInstantiate::Building,
837                deserialize_fn: None,
838                metadata: target_consistency.new_node_metadata(Stream::<
839                    Result<<Codec as Decoder>::Item, <Codec as Decoder>::Error>,
840                    Self::DropConsistency,
841                    Unbounded,
842                    TotalOrder,
843                    ExactlyOnce,
844                >::collection_kind(
845                )),
846            },
847        );
848
849        (
850            ExternalBytesPort {
851                process_key: from.key,
852                port_id: next_external_port_id,
853                _phantom: PhantomData,
854            },
855            raw_stream.flatten_ordered(),
856            fwd_ref,
857        )
858    }
859
860    // TODO: Replace this staged-expression helper with codec-parameterized sink and bidi handles.
861    #[doc(hidden)]
862    #[cfg(feature = "tokio")]
863    #[expect(clippy::type_complexity, reason = "stream markers")]
864    fn register_serialized_single_client<L, InT, OutT>(
865        &self,
866        from: &External<'_, L>,
867        serialize_fn: syn::Expr,
868        deserialize_fn: syn::Expr,
869    ) -> (
870        ExternalPortId,
871        Stream<InT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
872        ForwardHandle<'a, Stream<OutT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>>,
873    )
874    where
875        Self: TopLevel<'a> + Sized,
876    {
877        let next_external_port_id = from.flow_state.borrow_mut().next_external_port();
878
879        let target_consistency = self.drop_consistency();
880        let (fwd_ref, to_sink) = target_consistency.forward_ref::<Stream<
881            OutT,
882            Self::DropConsistency,
883            Unbounded,
884            TotalOrder,
885            ExactlyOnce,
886        >>();
887        let mut flow_state_borrow = self.flow_state().borrow_mut();
888
889        flow_state_borrow.push_root(HydroRoot::SendExternal {
890            to_external_key: from.key,
891            to_port_id: next_external_port_id,
892            to_many: false,
893            unpaired: false,
894            serialize_fn: Some(serialize_fn.into()),
895            instantiate_fn: DebugInstantiate::Building,
896            input: Box::new(to_sink.ir_node.replace(HydroNode::Placeholder)),
897            op_metadata: HydroIrOpMetadata::new(),
898        });
899        drop(flow_state_borrow);
900
901        let raw_stream: Stream<InT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce> =
902            Stream::new(
903                target_consistency.clone(),
904                HydroNode::ExternalInput {
905                    from_external_key: from.key,
906                    from_port_id: next_external_port_id,
907                    from_many: false,
908                    codec_type: quote_type::<LengthDelimitedCodec>().into(),
909                    port_hint: NetworkHint::Auto,
910                    instantiate_fn: DebugInstantiate::Building,
911                    deserialize_fn: Some(deserialize_fn.into()),
912                    metadata: target_consistency.new_node_metadata(Stream::<
913                        InT,
914                        Self::DropConsistency,
915                        Unbounded,
916                        TotalOrder,
917                        ExactlyOnce,
918                    >::collection_kind(
919                    )),
920                },
921            );
922
923        (next_external_port_id, raw_stream, fwd_ref)
924    }
925
926    /// Establishes a bidirectional connection from a single external client using bincode serialization.
927    ///
928    /// Returns a port handle for the external process to connect to, a stream of incoming messages,
929    /// and a handle to send outgoing messages. This is a convenience wrapper around
930    /// [`Location::bind_single_client`] that uses bincode for serialization.
931    ///
932    /// # Type Parameters
933    /// - `InT`: The type of incoming messages (must implement [`DeserializeOwned`])
934    /// - `OutT`: The type of outgoing messages (must implement [`Serialize`])
935    #[cfg(feature = "tokio")]
936    #[expect(clippy::type_complexity, reason = "stream markers")]
937    fn bind_single_client_bincode<L, InT: DeserializeOwned, OutT: Serialize>(
938        &self,
939        from: &External<'_, L>,
940    ) -> (
941        ExternalBincodeBidi<InT, OutT, NotMany>,
942        Stream<InT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
943        ForwardHandle<'a, Stream<OutT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>>,
944    )
945    where
946        Self: TopLevel<'a> + Sized,
947    {
948        let root = get_this_crate();
949
950        let out_t_type = quote_type::<OutT>();
951        let ser_fn: syn::Expr = syn::parse_quote! {
952            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<#out_t_type, _>(
953                |b| #root::runtime_support::bincode::serialize(&b).unwrap().into()
954            )
955        };
956
957        let in_t_type = quote_type::<InT>();
958        let deser_fn: syn::Expr = syn::parse_quote! {
959            |res| {
960                let b = res.unwrap();
961                #root::runtime_support::bincode::deserialize::<#in_t_type>(&b).unwrap()
962            }
963        };
964
965        let (port_id, raw_stream, fwd_ref) =
966            self.register_serialized_single_client::<_, InT, OutT>(from, ser_fn, deser_fn);
967
968        (
969            ExternalBincodeBidi {
970                process_key: from.key,
971                port_id,
972                _phantom: PhantomData,
973            },
974            raw_stream,
975            fwd_ref,
976        )
977    }
978
979    /// Establishes a server on this location to receive bidirectional connections from multiple
980    /// external clients using raw bytes.
981    ///
982    /// Unlike [`Location::bind_single_client`], this method supports multiple concurrent client
983    /// connections. Each client is assigned a unique `u64` identifier.
984    ///
985    /// Returns:
986    /// - A port handle for external processes to connect to
987    /// - A keyed stream of incoming messages, keyed by client ID
988    /// - A keyed stream of membership events (client joins/leaves), keyed by client ID
989    /// - A handle to send outgoing messages, keyed by client ID
990    #[cfg(feature = "tokio")]
991    #[expect(clippy::type_complexity, reason = "stream markers")]
992    fn bidi_external_many_bytes<L, T, Codec: Encoder<T> + Decoder>(
993        &self,
994        from: &External<'_, L>,
995        port_hint: NetworkHint,
996    ) -> (
997        ExternalBytesPort<Many>,
998        KeyedStream<
999            u64,
1000            <Codec as Decoder>::Item,
1001            Self::DropConsistency,
1002            Unbounded,
1003            TotalOrder,
1004            ExactlyOnce,
1005        >,
1006        KeyedStream<
1007            u64,
1008            MembershipEvent,
1009            Self::DropConsistency,
1010            Unbounded,
1011            TotalOrder,
1012            ExactlyOnce,
1013        >,
1014        ForwardHandle<
1015            'a,
1016            KeyedStream<u64, T, Self::DropConsistency, Unbounded, NoOrder, ExactlyOnce>,
1017        >,
1018    )
1019    where
1020        Self: TopLevel<'a> + Sized,
1021    {
1022        let next_external_port_id = from.flow_state.borrow_mut().next_external_port();
1023
1024        let target_consistency = self.drop_consistency();
1025        let (fwd_ref, to_sink) = target_consistency.forward_ref::<KeyedStream<
1026            u64,
1027            T,
1028            Self::DropConsistency,
1029            Unbounded,
1030            NoOrder,
1031            ExactlyOnce,
1032        >>();
1033        let to_sink_input = Box::new(to_sink.entries().ir_node.replace(HydroNode::Placeholder));
1034        let mut flow_state_borrow = self.flow_state().borrow_mut();
1035
1036        flow_state_borrow.push_root(HydroRoot::SendExternal {
1037            to_external_key: from.key,
1038            to_port_id: next_external_port_id,
1039            to_many: true,
1040            unpaired: false,
1041            serialize_fn: None,
1042            instantiate_fn: DebugInstantiate::Building,
1043            input: to_sink_input,
1044            op_metadata: HydroIrOpMetadata::new(),
1045        });
1046        drop(flow_state_borrow);
1047
1048        let raw_stream: Stream<
1049            Result<(u64, <Codec as Decoder>::Item), <Codec as Decoder>::Error>,
1050            Self::DropConsistency,
1051            Unbounded,
1052            TotalOrder,
1053            ExactlyOnce,
1054        > = Stream::new(
1055            target_consistency.clone(),
1056            HydroNode::ExternalInput {
1057                from_external_key: from.key,
1058                from_port_id: next_external_port_id,
1059                from_many: true,
1060                codec_type: quote_type::<Codec>().into(),
1061                port_hint,
1062                instantiate_fn: DebugInstantiate::Building,
1063                deserialize_fn: None,
1064                metadata: target_consistency.new_node_metadata(Stream::<
1065                    Result<(u64, <Codec as Decoder>::Item), <Codec as Decoder>::Error>,
1066                    Self::DropConsistency,
1067                    Unbounded,
1068                    TotalOrder,
1069                    ExactlyOnce,
1070                >::collection_kind(
1071                )),
1072            },
1073        );
1074
1075        let membership_stream_ident = syn::Ident::new(
1076            &format!(
1077                "__hydro_deploy_many_{}_{}_membership",
1078                from.key, next_external_port_id
1079            ),
1080            Span::call_site(),
1081        );
1082        let membership_stream_expr: syn::Expr = parse_quote!(#membership_stream_ident);
1083        let raw_membership_stream: KeyedStream<
1084            u64,
1085            bool,
1086            Self::DropConsistency,
1087            Unbounded,
1088            TotalOrder,
1089            ExactlyOnce,
1090        > = KeyedStream::new(
1091            target_consistency.clone(),
1092            HydroNode::Source {
1093                source: HydroSource::Stream(membership_stream_expr.into()),
1094                metadata: target_consistency.new_node_metadata(KeyedStream::<
1095                    u64,
1096                    bool,
1097                    Self::DropConsistency,
1098                    Unbounded,
1099                    TotalOrder,
1100                    ExactlyOnce,
1101                >::collection_kind(
1102                )),
1103            },
1104        );
1105
1106        (
1107            ExternalBytesPort {
1108                process_key: from.key,
1109                port_id: next_external_port_id,
1110                _phantom: PhantomData,
1111            },
1112            raw_stream
1113                .flatten_ordered() // TODO(shadaj): this silently drops framing errors, decide on right defaults
1114                .into_keyed(),
1115            raw_membership_stream.map(q!(|join| {
1116                if join {
1117                    MembershipEvent::Joined
1118                } else {
1119                    MembershipEvent::Left
1120                }
1121            })),
1122            fwd_ref,
1123        )
1124    }
1125
1126    /// Establishes a server on this location to receive bidirectional connections from multiple
1127    /// external clients using bincode serialization.
1128    ///
1129    /// Unlike [`Location::bind_single_client_bincode`], this method supports multiple concurrent
1130    /// client connections. Each client is assigned a unique `u64` identifier.
1131    ///
1132    /// Returns:
1133    /// - A port handle for external processes to connect to
1134    /// - A keyed stream of incoming messages, keyed by client ID
1135    /// - A keyed stream of membership events (client joins/leaves), keyed by client ID
1136    /// - A handle to send outgoing messages, keyed by client ID
1137    ///
1138    /// # Type Parameters
1139    /// - `InT`: The type of incoming messages (must implement [`DeserializeOwned`])
1140    /// - `OutT`: The type of outgoing messages (must implement [`Serialize`])
1141    #[cfg(feature = "tokio")]
1142    #[expect(clippy::type_complexity, reason = "stream markers")]
1143    fn bidi_external_many_bincode<L, InT: DeserializeOwned, OutT: Serialize>(
1144        &self,
1145        from: &External<'_, L>,
1146    ) -> (
1147        ExternalBincodeBidi<InT, OutT, Many>,
1148        KeyedStream<u64, InT, Self::DropConsistency, Unbounded, TotalOrder, ExactlyOnce>,
1149        KeyedStream<
1150            u64,
1151            MembershipEvent,
1152            Self::DropConsistency,
1153            Unbounded,
1154            TotalOrder,
1155            ExactlyOnce,
1156        >,
1157        ForwardHandle<
1158            'a,
1159            KeyedStream<u64, OutT, Self::DropConsistency, Unbounded, NoOrder, ExactlyOnce>,
1160        >,
1161    )
1162    where
1163        Self: TopLevel<'a> + Sized,
1164    {
1165        let next_external_port_id = from.flow_state.borrow_mut().next_external_port();
1166
1167        let target_consistency = self.drop_consistency();
1168        let (fwd_ref, to_sink) = target_consistency.forward_ref::<KeyedStream<
1169            u64,
1170            OutT,
1171            Self::DropConsistency,
1172            Unbounded,
1173            NoOrder,
1174            ExactlyOnce,
1175        >>();
1176        let to_sink_input = Box::new(to_sink.entries().ir_node.replace(HydroNode::Placeholder));
1177        let mut flow_state_borrow = self.flow_state().borrow_mut();
1178
1179        let root = get_this_crate();
1180
1181        let out_t_type = quote_type::<OutT>();
1182        let ser_fn: syn::Expr = syn::parse_quote! {
1183            #root::runtime_support::stageleft::runtime_support::fn1_type_hint::<(u64, #out_t_type), _>(
1184                |(id, b)| (id, #root::runtime_support::bincode::serialize(&b).unwrap().into())
1185            )
1186        };
1187
1188        flow_state_borrow.push_root(HydroRoot::SendExternal {
1189            to_external_key: from.key,
1190            to_port_id: next_external_port_id,
1191            to_many: true,
1192            unpaired: false,
1193            serialize_fn: Some(ser_fn.into()),
1194            instantiate_fn: DebugInstantiate::Building,
1195            input: to_sink_input,
1196            op_metadata: HydroIrOpMetadata::new(),
1197        });
1198        drop(flow_state_borrow);
1199
1200        let in_t_type = quote_type::<InT>();
1201
1202        let deser_fn: syn::Expr = syn::parse_quote! {
1203            |res| {
1204                let (id, b) = res.unwrap();
1205                (id, #root::runtime_support::bincode::deserialize::<#in_t_type>(&b).unwrap())
1206            }
1207        };
1208
1209        let raw_stream: KeyedStream<
1210            u64,
1211            InT,
1212            Self::DropConsistency,
1213            Unbounded,
1214            TotalOrder,
1215            ExactlyOnce,
1216        > = KeyedStream::new(
1217            target_consistency.clone(),
1218            HydroNode::ExternalInput {
1219                from_external_key: from.key,
1220                from_port_id: next_external_port_id,
1221                from_many: true,
1222                codec_type: quote_type::<LengthDelimitedCodec>().into(),
1223                port_hint: NetworkHint::Auto,
1224                instantiate_fn: DebugInstantiate::Building,
1225                deserialize_fn: Some(deser_fn.into()),
1226                metadata: target_consistency.new_node_metadata(KeyedStream::<
1227                    u64,
1228                    InT,
1229                    Self::DropConsistency,
1230                    Unbounded,
1231                    TotalOrder,
1232                    ExactlyOnce,
1233                >::collection_kind(
1234                )),
1235            },
1236        );
1237
1238        let membership_stream_ident = syn::Ident::new(
1239            &format!(
1240                "__hydro_deploy_many_{}_{}_membership",
1241                from.key, next_external_port_id
1242            ),
1243            Span::call_site(),
1244        );
1245        let membership_stream_expr: syn::Expr = parse_quote!(#membership_stream_ident);
1246        let raw_membership_stream: KeyedStream<
1247            u64,
1248            bool,
1249            Self::DropConsistency,
1250            Unbounded,
1251            TotalOrder,
1252            ExactlyOnce,
1253        > = KeyedStream::new(
1254            target_consistency.clone(),
1255            HydroNode::Source {
1256                source: HydroSource::Stream(membership_stream_expr.into()),
1257                metadata: target_consistency.new_node_metadata(KeyedStream::<
1258                    u64,
1259                    bool,
1260                    Self::DropConsistency,
1261                    Unbounded,
1262                    TotalOrder,
1263                    ExactlyOnce,
1264                >::collection_kind(
1265                )),
1266            },
1267        );
1268
1269        (
1270            ExternalBincodeBidi {
1271                process_key: from.key,
1272                port_id: next_external_port_id,
1273                _phantom: PhantomData,
1274            },
1275            raw_stream,
1276            raw_membership_stream.map(q!(|join| {
1277                if join {
1278                    MembershipEvent::Joined
1279                } else {
1280                    MembershipEvent::Left
1281                }
1282            })),
1283            fwd_ref,
1284        )
1285    }
1286
1287    /// Bridges user-owned async code to the dataflow as a **bidirectional sidecar**.
1288    ///
1289    /// The closure is called once at startup and must return a
1290    /// `(Stream<InT>, Sink<OutT>)` pair. The framework reads from the stream
1291    /// (items flowing *into* the dataflow) and writes to the sink (items flowing
1292    /// *out* to the sidecar). The user controls buffering, backpressure, and
1293    /// internal lifecycle — Hydro only sees the stream/sink interface.
1294    ///
1295    /// This will hopefully make it easy to integrate hydro with existing frameworks,
1296    /// for example grpc code generated service endpoints.
1297    ///
1298    /// # Returns
1299    /// - A `Stream<InT>` carrying items from the sidecar into the dataflow.
1300    /// - A [`ForwardHandle`] expecting a `Stream<OutT>` that the user completes
1301    ///   with items destined for the sidecar.
1302    ///
1303    /// # Example
1304    ///
1305    /// ```rust
1306    /// # #[cfg(feature = "deploy")] {
1307    /// # use hydro_lang::prelude::*;
1308    /// # use futures::StreamExt;
1309    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1310    /// // Sidecar that echoes whatever it receives back into the dataflow.
1311    /// let (inbound, response_handle) = process.sidecar_bidi::<String, String, _>(q!(|| {
1312    ///     let (to_df_tx, to_df_rx) = tokio::sync::mpsc::channel::<String>(16);
1313    ///     let (from_df_tx, mut from_df_rx) = tokio::sync::mpsc::channel::<String>(16);
1314    ///
1315    ///     // Spawn the sidecar: echoes items from the dataflow back into it.
1316    ///     tokio::spawn(async move {
1317    ///         while let Some(msg) = from_df_rx.recv().await {
1318    ///             to_df_tx.send(msg).await.ok();
1319    ///         }
1320    ///     });
1321    ///
1322    ///     // Return the framework-facing ends (concrete types, no boxing needed).
1323    ///     let stream = tokio_stream::wrappers::ReceiverStream::new(to_df_rx);
1324    ///     let sink = tokio_util::sync::PollSender::new(from_df_tx);
1325    ///     (stream, sink)
1326    /// }));
1327    ///
1328    /// // Send "hello" into the sidecar via the response channel.
1329    /// let input = process.source_stream(q!(futures::stream::iter(vec!["hello".to_string()])));
1330    /// response_handle.complete(input);
1331    ///
1332    /// // The sidecar echoes it back — assert we get "hello" out.
1333    /// inbound
1334    /// # }, |mut stream| async move {
1335    /// #     assert_eq!(stream.next().await.unwrap(), "hello");
1336    /// # }));
1337    /// # }
1338    /// ```
1339    fn sidecar_bidi<InT: 'static, OutT: 'static, F>(
1340        &self,
1341        sidecar: impl QuotedWithContext<'a, F, Self>,
1342    ) -> (
1343        Stream<InT, Self, Unbounded, TotalOrder, ExactlyOnce>,
1344        ForwardHandle<'a, Stream<OutT, Self, Unbounded, NoOrder, ExactlyOnce>>,
1345    )
1346    where
1347        Self: Sized + TopLevel<'a>,
1348    {
1349        let location_key = Location::id(self).key();
1350
1351        let sidecar_id = self.flow_state().borrow_mut().next_sidecar_id();
1352        let (stream_ident, sink_ident) = sidecar_id.idents();
1353
1354        let sidecar_closure: syn::Expr = sidecar.splice_untyped_ctx(self);
1355        self.flow_state()
1356            .borrow_mut()
1357            .sidecars
1358            .push(crate::compile::builder::Sidecar::Bidi {
1359                location_key,
1360                sidecar_id,
1361                sidecar_closure: Box::new(sidecar_closure),
1362            });
1363
1364        // Inbound stream: reads from the stream returned by the sidecar closure
1365        let source_expr: syn::Expr = parse_quote! {
1366            #stream_ident
1367        };
1368        let inbound: Stream<InT, Self, Unbounded, TotalOrder, ExactlyOnce> = Stream::new(
1369            self.clone(),
1370            HydroNode::Source {
1371                source: HydroSource::Stream(source_expr.into()),
1372                metadata: self.new_node_metadata(Stream::<
1373                    InT,
1374                    Self,
1375                    Unbounded,  // TODO: maybe bounded sidecars are interesting..?
1376                    TotalOrder, // TODO: NoOrder..?
1377                    ExactlyOnce,
1378                >::collection_kind()),
1379            },
1380        );
1381
1382        // Outbound: forward_ref cycle feeding the sink returned by the sidecar closure
1383        let (fwd_ref, to_sink): (
1384            ForwardHandle<'a, Stream<OutT, Self, Unbounded, NoOrder, ExactlyOnce>>,
1385            Stream<OutT, Self, Unbounded, NoOrder, ExactlyOnce>,
1386        ) = self.forward_ref();
1387
1388        let sink_expr: syn::Expr = parse_quote! {
1389            #sink_ident
1390        };
1391
1392        let sink_input_ir = to_sink.ir_node.replace(HydroNode::Placeholder);
1393        self.flow_state()
1394            .borrow_mut()
1395            .try_push_root(HydroRoot::DestSink {
1396                sink: sink_expr.into(),
1397                input: Box::new(sink_input_ir),
1398                op_metadata: HydroIrOpMetadata::new(),
1399            });
1400
1401        (inbound, fwd_ref)
1402    }
1403
1404    /// Constructs a [`Singleton`] materialized at this location with the given static value.
1405    ///
1406    /// See also: [`Tick::singleton`], for creating a singleton _within_ a tick, which requires
1407    /// `T: Clone`.
1408    ///
1409    /// # Example
1410    /// ```rust
1411    /// # #[cfg(feature = "deploy")] {
1412    /// # use hydro_lang::prelude::*;
1413    /// # use futures::StreamExt;
1414    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1415    /// let singleton = process.singleton(q!(5));
1416    /// # singleton.into_stream()
1417    /// # }, |mut stream| async move {
1418    /// // 5
1419    /// # assert_eq!(stream.next().await.unwrap(), 5);
1420    /// # }));
1421    /// # }
1422    /// ```
1423    fn singleton<T>(
1424        &self,
1425        e: impl QuotedWithContext<'a, T, Self>,
1426    ) -> Singleton<T, Self::DropConsistency, Bounded>
1427    where
1428        Self: Sized,
1429    {
1430        let e = e.splice_untyped_ctx(self);
1431
1432        let target_location = self.drop_consistency();
1433        Singleton::new(
1434            target_location.clone(),
1435            HydroNode::SingletonSource {
1436                value: e.into(),
1437                first_tick_only: false,
1438                metadata: target_location.new_node_metadata(Singleton::<
1439                    T,
1440                    Self::DropConsistency,
1441                    Bounded,
1442                >::collection_kind()),
1443            },
1444        )
1445    }
1446
1447    /// Constructs a [`Singleton`] by resolving an async [`Future`] to completion.
1448    ///
1449    /// This is a convenience method equivalent to
1450    /// `self.singleton(future_expr).resolve_future_blocking()`, which is a common
1451    /// pattern when initializing a singleton from an async computation.
1452    ///
1453    /// # Example
1454    /// ```rust
1455    /// # #[cfg(feature = "deploy")] {
1456    /// # use hydro_lang::prelude::*;
1457    /// # use futures::StreamExt;
1458    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1459    /// let singleton = process.singleton_future(q!(async { 42 }));
1460    /// singleton.into_stream()
1461    /// # }, |mut stream| async move {
1462    /// // 42
1463    /// # assert_eq!(stream.next().await.unwrap(), 42);
1464    /// # }));
1465    /// # }
1466    /// ```
1467    ///
1468    /// [`Future`]: std::future::Future
1469    fn singleton_future<F>(
1470        &self,
1471        e: impl QuotedWithContext<'a, F, Self>,
1472    ) -> Singleton<F::Output, Self::DropConsistency, Bounded>
1473    where
1474        F: Future,
1475        Self: Sized,
1476    {
1477        self.singleton(e).resolve_future_blocking()
1478    }
1479
1480    /// Generates a stream that emits `()` at a fixed interval.
1481    ///
1482    /// The first tick completes immediately. Missed ticks will be scheduled
1483    /// as soon as possible.
1484    ///
1485    /// Because this only emits `()`, the non-determinism of *when* events fire
1486    /// is captured by the `AtLeastOnce` retry semantics downstream, so no
1487    /// [`NonDet`] guard is required.
1488    #[cfg(feature = "tokio")]
1489    fn source_interval(
1490        &self,
1491        interval: impl QuotedWithContext<'a, Duration, Self> + Copy + 'a,
1492    ) -> Stream<(), Self, Unbounded, TotalOrder, ExactlyOnce>
1493    where
1494        Self: TopLevel<'a> + Sized,
1495    {
1496        self.source_stream(q!(tokio_stream::StreamExt::map(
1497            tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(interval)),
1498            |_| ()
1499        )))
1500        .assert_has_consistency_of_trusted(
1501            manual_proof!(/** interval does not reveal timestamps */),
1502        )
1503    }
1504
1505    /// Generates a stream that emits `()` at a fixed interval, after an
1506    /// initial delay.
1507    ///
1508    /// Because this only emits `()`, the non-determinism of *when* events fire
1509    /// is captured by the `AtLeastOnce` retry semantics downstream, so no
1510    /// [`NonDet`] guard is required.
1511    #[cfg(feature = "tokio")]
1512    fn source_interval_delayed(
1513        &self,
1514        delay: impl QuotedWithContext<'a, Duration, Self> + Copy + 'a,
1515        interval: impl QuotedWithContext<'a, Duration, Self> + Copy + 'a,
1516    ) -> Stream<(), Self, Unbounded, TotalOrder, ExactlyOnce>
1517    where
1518        Self: TopLevel<'a> + Sized,
1519    {
1520        self.source_stream(q!(tokio_stream::StreamExt::map(
1521            tokio_stream::wrappers::IntervalStream::new(tokio::time::interval_at(
1522                tokio::time::Instant::now() + delay,
1523                interval,
1524            )),
1525            |_| ()
1526        )))
1527        .assert_has_consistency_of_trusted(
1528            manual_proof!(/** interval does not reveal timestamps */),
1529        )
1530    }
1531
1532    /// Creates a forward reference, allowing a stream to be used before its source is defined.
1533    ///
1534    /// Returns a `(handle, placeholder)` pair. Use the placeholder in the dataflow graph,
1535    /// then call `handle.complete(actual_stream)` to wire in the real source.
1536    ///
1537    /// This is useful for mutually-dependent dataflows or when the definition order
1538    /// doesn't match the data flow direction. For feedback loops, prefer [`Tick::cycle`]
1539    /// instead, which automatically defers values by one tick.
1540    ///
1541    /// # Panics
1542    /// Panics if the forward reference creates a synchronous cycle (i.e., the completed
1543    /// stream transitively depends on the placeholder without a `defer_tick` or network
1544    /// hop in between).
1545    ///
1546    /// # Example
1547    /// ```rust
1548    /// # #[cfg(feature = "deploy")] {
1549    /// # use hydro_lang::prelude::*;
1550    /// # use hydro_lang::live_collections::stream::NoOrder;
1551    /// # use futures::StreamExt;
1552    /// # tokio_test::block_on(hydro_lang::test_util::stream_transform_test(|process| {
1553    /// // Create a forward reference to define a stream that will be completed later
1554    /// let (complete, forward_stream) = process.forward_ref::<Stream<i32, _, _, NoOrder>>();
1555    ///
1556    /// // Use the forward reference as input to another computation
1557    /// let output: Stream<_, _, _, NoOrder> = forward_stream.map(q!(|x| x * 2));
1558    ///
1559    /// // Complete the forward reference with the actual source
1560    /// let source: Stream<_, _, Unbounded> = process.source_iter(q!([1, 2, 3])).into();
1561    /// complete.complete(source);
1562    /// output
1563    /// # }, |mut stream| async move {
1564    /// // 2, 4, 6
1565    /// # assert_eq!(stream.next().await.unwrap(), 2);
1566    /// # assert_eq!(stream.next().await.unwrap(), 4);
1567    /// # assert_eq!(stream.next().await.unwrap(), 6);
1568    /// # }));
1569    /// # }
1570    /// ```
1571    fn forward_ref<S>(&self) -> (ForwardHandle<'a, S>, S)
1572    where
1573        S: CycleCollection<'a, ForwardRef, Location = Self>,
1574    {
1575        let cycle_id = self.flow_state().borrow_mut().next_cycle_id();
1576        (
1577            ForwardHandle::new(cycle_id, Location::id(self)),
1578            S::create_source(cycle_id, self.clone()),
1579        )
1580    }
1581}
1582
1583#[cfg(feature = "deploy")]
1584#[cfg(test)]
1585mod tests {
1586    use std::collections::HashSet;
1587
1588    use futures::{SinkExt, StreamExt};
1589    use hydro_deploy::Deployment;
1590    use stageleft::q;
1591    use tokio_util::codec::LengthDelimitedCodec;
1592
1593    use crate::compile::builder::FlowBuilder;
1594    use crate::live_collections::stream::{ExactlyOnce, TotalOrder};
1595    use crate::location::{Location, NetworkHint};
1596    use crate::nondet::nondet;
1597
1598    #[tokio::test]
1599    async fn top_level_singleton_replay_cardinality() {
1600        let mut deployment = Deployment::new();
1601
1602        let mut flow = FlowBuilder::new();
1603        let node = flow.process::<()>();
1604        let external = flow.external::<()>();
1605
1606        let (in_port, input) =
1607            node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
1608        let singleton = node.singleton(q!(123));
1609        let tick = node.tick();
1610        let out = input
1611            .batch(&tick, nondet!(/** test */))
1612            .cross_singleton(singleton.clone().snapshot(&tick, nondet!(/** test */)))
1613            .cross_singleton(
1614                singleton
1615                    .snapshot(&tick, nondet!(/** test */))
1616                    .into_stream()
1617                    .count(),
1618            )
1619            .all_ticks()
1620            .send_bincode_external(&external);
1621
1622        let nodes = flow
1623            .with_process(&node, deployment.Localhost())
1624            .with_external(&external, deployment.Localhost())
1625            .deploy(&mut deployment);
1626
1627        deployment.deploy().await.unwrap();
1628
1629        let mut external_in = nodes.connect(in_port).await;
1630        let mut external_out = nodes.connect(out).await;
1631
1632        deployment.start().await.unwrap();
1633
1634        external_in.send(1).await.unwrap();
1635        assert_eq!(external_out.next().await.unwrap(), ((1, 123), 1));
1636
1637        external_in.send(2).await.unwrap();
1638        assert_eq!(external_out.next().await.unwrap(), ((2, 123), 1));
1639    }
1640
1641    #[tokio::test]
1642    async fn tick_singleton_replay_cardinality() {
1643        let mut deployment = Deployment::new();
1644
1645        let mut flow = FlowBuilder::new();
1646        let node = flow.process::<()>();
1647        let external = flow.external::<()>();
1648
1649        let (in_port, input) =
1650            node.source_external_bincode::<_, _, TotalOrder, ExactlyOnce>(&external);
1651        let tick = node.tick();
1652        let singleton = tick.singleton(q!(123));
1653        let out = input
1654            .batch(&tick, nondet!(/** test */))
1655            .cross_singleton(singleton.clone())
1656            .cross_singleton(singleton.into_stream().count())
1657            .all_ticks()
1658            .send_bincode_external(&external);
1659
1660        let nodes = flow
1661            .with_process(&node, deployment.Localhost())
1662            .with_external(&external, deployment.Localhost())
1663            .deploy(&mut deployment);
1664
1665        deployment.deploy().await.unwrap();
1666
1667        let mut external_in = nodes.connect(in_port).await;
1668        let mut external_out = nodes.connect(out).await;
1669
1670        deployment.start().await.unwrap();
1671
1672        external_in.send(1).await.unwrap();
1673        assert_eq!(external_out.next().await.unwrap(), ((1, 123), 1));
1674
1675        external_in.send(2).await.unwrap();
1676        assert_eq!(external_out.next().await.unwrap(), ((2, 123), 1));
1677    }
1678
1679    #[tokio::test]
1680    async fn external_bytes() {
1681        let mut deployment = Deployment::new();
1682
1683        let mut flow = FlowBuilder::new();
1684        let first_node = flow.process::<()>();
1685        let external = flow.external::<()>();
1686
1687        let (in_port, input) = first_node.source_external_bytes(&external);
1688        let out = input.send_bincode_external(&external);
1689
1690        let nodes = flow
1691            .with_process(&first_node, deployment.Localhost())
1692            .with_external(&external, deployment.Localhost())
1693            .deploy(&mut deployment);
1694
1695        deployment.deploy().await.unwrap();
1696
1697        let mut external_in = nodes.connect(in_port).await.1;
1698        let mut external_out = nodes.connect(out).await;
1699
1700        deployment.start().await.unwrap();
1701
1702        external_in.send(vec![1, 2, 3].into()).await.unwrap();
1703
1704        assert_eq!(external_out.next().await.unwrap(), vec![1, 2, 3]);
1705    }
1706
1707    #[tokio::test]
1708    async fn multi_external_source() {
1709        let mut deployment = Deployment::new();
1710
1711        let mut flow = FlowBuilder::new();
1712        let first_node = flow.process::<()>();
1713        let external = flow.external::<()>();
1714
1715        let (in_port, input, _membership, complete_sink) =
1716            first_node.bidi_external_many_bincode(&external);
1717        let out = input.entries().send_bincode_external(&external);
1718        complete_sink.complete(
1719            first_node
1720                .source_iter::<(u64, ()), _>(q!([]))
1721                .into_keyed()
1722                .weaken_ordering(),
1723        );
1724
1725        let nodes = flow
1726            .with_process(&first_node, deployment.Localhost())
1727            .with_external(&external, deployment.Localhost())
1728            .deploy(&mut deployment);
1729
1730        deployment.deploy().await.unwrap();
1731
1732        let (_, mut external_in_1) = nodes.connect_bincode(in_port.clone()).await;
1733        let (_, mut external_in_2) = nodes.connect_bincode(in_port).await;
1734        let external_out = nodes.connect(out).await;
1735
1736        deployment.start().await.unwrap();
1737
1738        external_in_1.send(123).await.unwrap();
1739        external_in_2.send(456).await.unwrap();
1740
1741        assert_eq!(
1742            external_out.take(2).collect::<HashSet<_>>().await,
1743            vec![(0, 123), (1, 456)].into_iter().collect()
1744        );
1745    }
1746
1747    #[tokio::test]
1748    async fn second_connection_only_multi_source() {
1749        let mut deployment = Deployment::new();
1750
1751        let mut flow = FlowBuilder::new();
1752        let first_node = flow.process::<()>();
1753        let external = flow.external::<()>();
1754
1755        let (in_port, input, _membership, complete_sink) =
1756            first_node.bidi_external_many_bincode(&external);
1757        let out = input.entries().send_bincode_external(&external);
1758        complete_sink.complete(
1759            first_node
1760                .source_iter::<(u64, ()), _>(q!([]))
1761                .into_keyed()
1762                .weaken_ordering(),
1763        );
1764
1765        let nodes = flow
1766            .with_process(&first_node, deployment.Localhost())
1767            .with_external(&external, deployment.Localhost())
1768            .deploy(&mut deployment);
1769
1770        deployment.deploy().await.unwrap();
1771
1772        // intentionally skipped to test stream waking logic
1773        let (_, mut _external_in_1) = nodes.connect_bincode(in_port.clone()).await;
1774        let (_, mut external_in_2) = nodes.connect_bincode(in_port).await;
1775        let mut external_out = nodes.connect(out).await;
1776
1777        deployment.start().await.unwrap();
1778
1779        external_in_2.send(456).await.unwrap();
1780
1781        assert_eq!(external_out.next().await.unwrap(), (1, 456));
1782    }
1783
1784    #[tokio::test]
1785    async fn multi_external_bytes() {
1786        let mut deployment = Deployment::new();
1787
1788        let mut flow = FlowBuilder::new();
1789        let first_node = flow.process::<()>();
1790        let external = flow.external::<()>();
1791
1792        let (in_port, input, _membership, complete_sink) = first_node
1793            .bidi_external_many_bytes::<_, _, LengthDelimitedCodec>(&external, NetworkHint::Auto);
1794        let out = input.entries().send_bincode_external(&external);
1795        complete_sink.complete(
1796            first_node
1797                .source_iter(q!([]))
1798                .into_keyed()
1799                .weaken_ordering(),
1800        );
1801
1802        let nodes = flow
1803            .with_process(&first_node, deployment.Localhost())
1804            .with_external(&external, deployment.Localhost())
1805            .deploy(&mut deployment);
1806
1807        deployment.deploy().await.unwrap();
1808
1809        let mut external_in_1 = nodes.connect(in_port.clone()).await.1;
1810        let mut external_in_2 = nodes.connect(in_port).await.1;
1811        let external_out = nodes.connect(out).await;
1812
1813        deployment.start().await.unwrap();
1814
1815        external_in_1.send(vec![1, 2, 3].into()).await.unwrap();
1816        external_in_2.send(vec![4, 5].into()).await.unwrap();
1817
1818        assert_eq!(
1819            external_out.take(2).collect::<HashSet<_>>().await,
1820            vec![
1821                (0, (&[1u8, 2, 3] as &[u8]).into()),
1822                (1, (&[4u8, 5] as &[u8]).into())
1823            ]
1824            .into_iter()
1825            .collect()
1826        );
1827    }
1828
1829    #[tokio::test]
1830    async fn single_client_external_bytes() {
1831        let mut deployment = Deployment::new();
1832        let mut flow = FlowBuilder::new();
1833        let first_node = flow.process::<()>();
1834        let external = flow.external::<()>();
1835        let (port, input, complete_sink) = first_node
1836            .bind_single_client::<_, _, LengthDelimitedCodec>(&external, NetworkHint::Auto);
1837        complete_sink.complete(input.map(q!(|data| {
1838            let mut resp: Vec<u8> = data.into();
1839            resp.push(42);
1840            resp.into() // : Bytes
1841        })));
1842
1843        let nodes = flow
1844            .with_process(&first_node, deployment.Localhost())
1845            .with_external(&external, deployment.Localhost())
1846            .deploy(&mut deployment);
1847
1848        deployment.deploy().await.unwrap();
1849        deployment.start().await.unwrap();
1850
1851        let (mut external_out, mut external_in) = nodes.connect(port).await;
1852
1853        external_in.send(vec![1, 2, 3].into()).await.unwrap();
1854        assert_eq!(
1855            external_out.next().await.unwrap().unwrap(),
1856            vec![1, 2, 3, 42]
1857        );
1858    }
1859
1860    #[tokio::test]
1861    async fn echo_external_bytes() {
1862        let mut deployment = Deployment::new();
1863
1864        let mut flow = FlowBuilder::new();
1865        let first_node = flow.process::<()>();
1866        let external = flow.external::<()>();
1867
1868        let (port, input, _membership, complete_sink) = first_node
1869            .bidi_external_many_bytes::<_, _, LengthDelimitedCodec>(&external, NetworkHint::Auto);
1870        complete_sink
1871            .complete(input.map(q!(|bytes| { bytes.into_iter().map(|x| x + 1).collect() })));
1872
1873        let nodes = flow
1874            .with_process(&first_node, deployment.Localhost())
1875            .with_external(&external, deployment.Localhost())
1876            .deploy(&mut deployment);
1877
1878        deployment.deploy().await.unwrap();
1879
1880        let (mut external_out_1, mut external_in_1) = nodes.connect(port.clone()).await;
1881        let (mut external_out_2, mut external_in_2) = nodes.connect(port).await;
1882
1883        deployment.start().await.unwrap();
1884
1885        external_in_1.send(vec![1, 2, 3].into()).await.unwrap();
1886        external_in_2.send(vec![4, 5].into()).await.unwrap();
1887
1888        assert_eq!(external_out_1.next().await.unwrap().unwrap(), vec![2, 3, 4]);
1889        assert_eq!(external_out_2.next().await.unwrap().unwrap(), vec![5, 6]);
1890    }
1891
1892    #[tokio::test]
1893    async fn echo_external_bincode() {
1894        let mut deployment = Deployment::new();
1895
1896        let mut flow = FlowBuilder::new();
1897        let first_node = flow.process::<()>();
1898        let external = flow.external::<()>();
1899
1900        let (port, input, _membership, complete_sink) =
1901            first_node.bidi_external_many_bincode(&external);
1902        complete_sink.complete(input.map(q!(|text: String| { text.to_uppercase() })));
1903
1904        let nodes = flow
1905            .with_process(&first_node, deployment.Localhost())
1906            .with_external(&external, deployment.Localhost())
1907            .deploy(&mut deployment);
1908
1909        deployment.deploy().await.unwrap();
1910
1911        let (mut external_out_1, mut external_in_1) = nodes.connect_bincode(port.clone()).await;
1912        let (mut external_out_2, mut external_in_2) = nodes.connect_bincode(port).await;
1913
1914        deployment.start().await.unwrap();
1915
1916        external_in_1.send("hi".to_owned()).await.unwrap();
1917        external_in_2.send("hello".to_owned()).await.unwrap();
1918
1919        assert_eq!(external_out_1.next().await.unwrap(), "HI");
1920        assert_eq!(external_out_2.next().await.unwrap(), "HELLO");
1921    }
1922
1923    #[tokio::test]
1924    async fn closure_location_name() {
1925        let mut deployment = Deployment::new();
1926        let mut flow = FlowBuilder::new();
1927
1928        enum ClosureProcess {}
1929
1930        let node = flow.process::<ClosureProcess>();
1931        let external = flow.external::<()>();
1932
1933        let (in_port, input) =
1934            node.source_external_bincode::<_, i32, TotalOrder, ExactlyOnce>(&external);
1935        let out = input.send_bincode_external(&external);
1936
1937        let nodes = flow
1938            .with_process(&node, deployment.Localhost())
1939            .with_external(&external, deployment.Localhost())
1940            .deploy(&mut deployment);
1941
1942        deployment.deploy().await.unwrap();
1943
1944        let mut external_in = nodes.connect(in_port).await;
1945        let mut external_out = nodes.connect(out).await;
1946
1947        deployment.start().await.unwrap();
1948
1949        external_in.send(42).await.unwrap();
1950        assert_eq!(external_out.next().await.unwrap(), 42);
1951    }
1952}