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