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