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