# irpc: Design Patterns and Usage Examples ## Pattern 1: Actor Model (Most Common) The primary usage pattern is an actor that receives messages and processes them sequentially: ```rust struct StorageActor { recv: tokio::sync::mpsc::Receiver, state: BTreeMap, } impl StorageActor { pub fn spawn() -> StorageApi { let (tx, rx) = tokio::sync::mpsc::channel(16); let actor = Self { recv: rx, state: BTreeMap::new() }; tokio::task::spawn(actor.run()); StorageApi { inner: Client::local(tx) } } async fn run(mut self) { while let Some(msg) = self.recv.recv().await { self.handle(msg).await; } } async fn handle(&mut self, msg: StorageMessage) { match msg { StorageMessage::Get(wc) => { let WithChannels { inner, tx, .. } = wc; tx.send(self.state.get(&inner.key).cloned()).await.ok(); } StorageMessage::Set(wc) => { let WithChannels { inner, tx, .. } = wc; self.state.insert(inner.key, inner.value); tx.send(()).await.ok(); } } } } ``` **Key points:** - The actor owns state and processes messages sequentially - `Client::local(tx)` wraps the sender side of the mpsc channel - `WithChannels` destructuring gives access to `inner` (the request data), `tx` (response channel), and `rx` (update channel) - The `..` pattern ignores `rx` when it's `NoReceiver` and `span` (with `spans` feature) ## Pattern 2: Concurrent Task Per Request For long-running or independent requests, spawn a task per message: ```rust async fn run(mut self) { while let Ok(Some(msg)) = self.recv.recv().await { tokio::task::spawn(async move { if let Err(cause) = Self::handle(msg).await { eprintln!("Error: {cause}"); } }); } } ``` This is useful for CPU-intensive or I/O-bound requests that shouldn't block other requests. ## Pattern 3: Local-Only Usage irpc can be used without any RPC feature for pure in-process communication: ```rust // Cargo.toml: default-features = false, features = ["derive"] #[rpc_requests(message = StorageMessage, no_rpc, no_spans)] #[derive(Serialize, Deserialize, Debug)] enum StorageProtocol { #[rpc(tx=oneshot::Sender>)] Get(Get), #[rpc(tx=oneshot::Sender<()>)] Set(Set), } ``` The `no_rpc` flag prevents `RemoteService` from being generated, and `no_spans` removes the tracing dependency. This leaves only the local channel mechanism, with minimal dependencies (serde, tokio, tokio-util). ## Pattern 4: API Type Wrapping Client The recommended pattern is to wrap `Client` in a higher-level API type: ```rust struct StorageApi { inner: Client, } impl StorageApi { // Local pub fn spawn() -> Self { let (tx, rx) = tokio::sync::mpsc::channel(16); tokio::task::spawn(StorageActor::new(rx).run()); Self { inner: Client::local(tx) } } // Remote (noq) pub fn connect(endpoint: noq::Endpoint, addr: SocketAddr) -> Self { Self { inner: Client::noq(endpoint, addr) } } // Remote (iroh) pub fn connect_iroh(endpoint: iroh::Endpoint, addr: EndpointAddr) -> Self { Self { inner: irpc_iroh::client(endpoint, addr, ALPN) } } // Type-safe methods that work for both local and remote pub async fn get(&self, key: String) -> irpc::Result> { self.inner.rpc(Get { key }).await } pub async fn set(&self, key: String, value: String) -> irpc::Result<()> { self.inner.rpc(Set { key, value }).await } pub async fn list(&self) -> irpc::Result> { self.inner.server_streaming(List, 16).await } } ``` This encapsulates the protocol details and provides a clean, type-safe API. The same `StorageApi` works identically whether connected locally or remotely. ## Pattern 5: Server Setup ### With noq ```rust fn serve(api: &StorageApi, endpoint: noq::Endpoint) -> Result> { let local = api.inner.as_local().context("cannot listen on remote service")?; let handler = StorageProtocol::remote_handler(local); Ok(tokio::task::spawn(irpc::rpc::listen(endpoint, handler))) } ``` ### With iroh ```rust fn serve(api: &StorageApi, endpoint: iroh::Endpoint) -> Result { let local = api.inner.as_local().context("cannot listen on remote service")?; let protocol = IrohProtocol::with_sender(local); Ok(Router::builder(endpoint).accept(ALPN, protocol).spawn()) } ``` ## Pattern 6: Low-Level Request Handling For more control than the `Client` methods provide, use `request()` directly: ```rust async fn custom_request(&self, msg: Get) -> anyhow::Result>> { match self.inner.request().await? { Request::Local(request) => { let (tx, rx) = oneshot::channel(); request.send((msg, tx)).await?; Ok(rx) } Request::Remote(request) => { let (_tx, rx) = request.write(msg).await?; Ok(rx.into()) } } } ``` This allows custom channel creation logic, e.g., different buffer sizes for local vs remote. ## Pattern 7: Channel Filtering and Mapping irpc channels support filtering and mapping, which work for both local and remote channels: ```rust // Server-side: filter responses to only include values > 10 let filtered_tx = wc.tx.with_filter(|v: &i64| *v > 10); // Server-side: transform responses let mapped_tx = wc.tx.with_map(|v: i64| v * 2); // Client-side: filter received updates let filtered_rx = rx.filter(|update: &Update| update.is_relevant()); ``` For remote channels, these create boxed wrappers. For local channels, they also create boxed wrappers. The overhead is negligible for remote (network latency dominates) but present for local. ## Pattern 8: Using the `wrap` Attribute The `#[wrap]` attribute generates named structs from variant fields: ```rust #[rpc_requests(message = StoreMessage)] #[derive(Debug, Serialize, Deserialize)] enum StoreProtocol { #[rpc(tx=oneshot::Sender>)] #[wrap(GetRequest, derive(Clone))] Get(String), // Generates: pub struct GetRequest(pub String); #[rpc(tx=oneshot::Sender<()>)] #[wrap(SetRequest)] Set { key: String, value: String }, // Generates: pub struct SetRequest { pub key: String, pub value: String } } ``` Benefits: - Named request types can be imported and constructed by name - Additional derives (e.g., `Clone`) can be added - Custom visibility can be specified: `#[wrap(pub(crate) GetRequest)]` - The generated struct inherits the enum's visibility by default ## Pattern 9: 0-RTT Connections For reduced latency on reconnections with iroh: ```rust // Client side let result = client.rpc_0rtt(Get { key: "x".into() }).await?; // Server side (iroh) let protocol = Iroh0RttProtocol::with_sender(local_sender); let router = Router::builder(endpoint).accept(ALPN, protocol).spawn(); ``` **Important:** Only use 0-RTT for idempotent operations, as the data may be replayed by an attacker. ## Pattern 10: Shared State in Actor For actors that need shared state accessible from multiple handlers: ```rust struct Actor { recv: tokio::sync::mpsc::Receiver, state: Arc>, } ``` Or use the actor pattern with internal mutation: ```rust struct Actor { recv: tokio::sync::mpsc::Receiver, db: HashMap, // owned state } ``` Since the actor processes messages sequentially, no internal synchronization is needed.