# tarpc-cat
RPC framework built on [comp-cat-rs](https://crates.io/crates/comp-cat-rs): typed effects, no async, categorical foundations.
Reimagines the core abstractions of [tarpc](https://crates.io/crates/tarpc) using `Io<E, A>` as the effect type, blocking I/O via `Io::suspend`, and thread-based concurrency. Nothing executes until `.run()`.
## Features
- **Lazy effects** -- all operations return `Io<Error, A>`, composable via `map`, `flat_map`, `zip`
- **Linear state-threading transport** -- the `Transport` trait consumes and returns itself on each operation, avoiding shared state
- **Connection-per-request client** -- simple, stateless TCP calls
- **Thread-per-connection server** -- accepts connections via iterator combinators, spawns a thread for each
- **Pluggable transport** -- `TcpTransport` for production, `ChannelTransport` for testing
- **Length-delimited JSON wire format** -- 4-byte big-endian length prefix + JSON payload
- **No async, no tokio** -- pure `std::net` + `comp-cat-rs` effects
## Usage
### Define a service
```rust
use tarpc_cat::serve::Serve;
use tarpc_cat::error::Error;
use comp_cat_rs::effect::io::Io;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Ping(String);
#[derive(Serialize, Deserialize)]
struct Pong(String);
#[derive(Clone)]
struct PingService;
impl Serve for PingService {
type Request = Ping;
type Response = Pong;
fn handle(&self, request: Ping) -> Io<Error, Pong> {
Io::pure(Pong(request.0))
}
}
```
### Run the server
```rust
use tarpc_cat::server::{serve, ListenAddr};
let addr = ListenAddr::new("127.0.0.1:9000".parse().unwrap());
serve(addr, PingService).run()?;
```
### Call from a client
```rust
use tarpc_cat::client::{call, ServerAddr};
let addr = ServerAddr::new("127.0.0.1:9000".parse().unwrap());
let pong: Pong = call(addr, Ping("hello".into())).run()?;
```
### Compose multiple calls
```rust
use tarpc_cat::client::{call, ServerAddr};
let addr = ServerAddr::new("127.0.0.1:9000".parse().unwrap());
let both = call::<Ping, Pong>(addr, Ping("first".into()))
.flat_map(move |pong1| {
call::<Ping, Pong>(addr, Ping("second".into()))
.map(move |pong2| (pong1, pong2))
});
let (a, b) = both.run()?;
```
### Use the transport directly
```rust
use tarpc_cat::client::call_on;
use tarpc_cat::transport::TcpTransport;
let transport = TcpTransport::connect("127.0.0.1:9000".parse().unwrap()).run()?;
let (response, transport) = call_on::<_, Ping, Pong>(transport, Ping("hello".into())).run()?;
// transport is available for another call
let (response2, _) = call_on::<_, Ping, Pong>(transport, Ping("again".into())).run()?;
```
## Architecture
```text
client.rs call() and call_on() -- connection-per-request RPC
server.rs serve() -- bind + accept loop + thread-per-connection
serve.rs Serve trait -- Clone + Send + 'static, handle returns Io
transport.rs Transport trait -- linear state-threading, TcpTransport, ChannelTransport
codec.rs Length-delimited JSON framing over Read/Write
protocol.rs Wire types: RequestId newtype, Envelope sum type
error.rs Error enum with hand-rolled Display/Error/From
```
## Building
```bash
cargo build
```
## Testing
```bash
cargo test
```
## Linting
```bash
RUSTFLAGS="-D warnings" cargo clippy
```
## Documentation
```bash
cargo doc --no-deps --open
```
Docs are auto-published to GitHub Pages on push to `main` via the workflow in `.github/workflows/docs.yml`. Enable in repo Settings > Pages > Source > GitHub Actions.
## License
Licensed under either of
- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
- [MIT License](http://opensource.org/licenses/MIT)
at your option.