wireframe 0.1.0

Simplify building servers and clients for custom binary protocols.
Documentation
# Wireframe

**Wireframe** is an experimental Rust library that simplifies building servers
and clients for custom binary protocols. The design borrows heavily from
[Actix Web](https://actix.rs/) to provide a familiar, declarative API for
routing, extractors, and middleware.

## Motivation

Manual handling of binary protocols typically involves verbose serialization
code, custom frame parsing, and complex dispatch logic. `wireframe` aims to
reduce this boilerplate through layered abstractions:

- **Transport adapter** built on Tokio I/O
- **Framing layer** for length‑prefixed or custom frames
- **Connection preamble** with customizable validation callbacks [^1]
- Call `with_preamble::<T>()` before registering success or failure callbacks
- **Serialization engine** using `bincode` or a `wire-rs` wrapper
- **Routing engine** that dispatches messages by ID
- **Handler invocation** with extractor support
- **Middleware chain** for request/response processing
- **[Connection lifecycle hooks]#connection-lifecycle** for per-connection
  setup and teardown

These layers correspond to the architecture outlined in the design document[^2].

## API overview

Applications are configured using a builder pattern similar to Actix Web. A
`WireframeApp` defines routes and middleware, while `WireframeServer` manages
connections and runs the Tokio event loop:

```rust
WireframeServer::new(|| {
    WireframeApp::new()
        .app_data(state.clone())
        .route(MessageType::Login, handle_login)
        .wrap(MyLoggingMiddleware::default())
})
.bind("127.0.0.1:7878")?
.run()
.await
```

By default, the number of worker tasks equals the number of CPU cores. If the
CPU count cannot be determined, the server falls back to a single worker.

The builder supports methods like `route`, `app_data`, and `wrap` for
middleware configuration. `app_data` stores any `Send + Sync` value keyed by
type; registering another value of the same type overwrites the previous one.
Handlers retrieve these values using the `SharedState<T>` extractor[^3].

Handlers are asynchronous functions whose parameters implement extractor traits
and may return responses implementing the `Responder` trait. This pattern
mirrors Actix Web handlers and keeps protocol logic concise[^4].

## Example

The design document includes a simple echo server that demonstrates routing
based on a message ID and the use of a length‑delimited codec:

```rust
async fn handle_echo(req: Message<EchoRequest>) -> WireframeResult<EchoResponse> {
    Ok(EchoResponse {
        original_payload: req.payload.clone(),
        echoed_at: time_now(),
    })
}

WireframeServer::new(|| {
    WireframeApp::new()
        .serializer(BincodeSerializer)
        .route(MyMessageType::Echo, handle_echo)
})
.bind("127.0.0.1:8000")?
.run()
.await
```

This example showcases how derive macros and the framing abstraction simplify a
binary protocol server. See the <!-- markdownlint-disable-next-line MD013 -->
[full example](docs/rust-binary-router-library-design.md#5-6-illustrative-api-usage-examples)
 in the design document for further details.

## Custom envelopes

`WireframeApp` defaults to a simple `Envelope` containing a message ID and raw
payload bytes. Applications can supply their own envelope type by calling
`WireframeApp::<_, _, MyEnv>::new()`. The custom type must implement the
`Packet` trait:

```rust
use wireframe::app::{Packet, PacketParts, WireframeApp};

#[derive(bincode::Encode, bincode::BorrowDecode)]
struct MyEnv { id: u32, correlation_id: Option<u64>, payload: Vec<u8> }

impl Packet for MyEnv {
    fn id(&self) -> u32 { self.id }
    fn correlation_id(&self) -> Option<u64> { self.correlation_id }
    fn into_parts(self) -> PacketParts {
        PacketParts::new(self.id, self.correlation_id, self.payload)
    }
    fn from_parts(parts: PacketParts) -> Self {
        let id = parts.id();
        let correlation_id = parts.correlation_id();
        let payload = parts.payload();
        Self { id, correlation_id, payload }
    }
}

let app = WireframeApp::<_, _, MyEnv>::new()
    .unwrap()
    .route(1, std::sync::Arc::new(|env: &MyEnv| Box::pin(async move { /* ... */ })))
    .unwrap();
```

A `None` correlation ID denotes an unsolicited event or server-initiated push.
Use `None` rather than `Some(0)` when a frame lacks a correlation ID. See
[PacketParts](docs/api.md#packetparts) for field details.

This allows integration with existing packet formats without modifying
`handle_frame`.

## Response serialization and framing

Handlers can return types implementing the `Responder` trait. These values are
encoded using the application's configured serializer and framed by a
length‑delimited codec[^5].

Frames are length prefixed using `tokio_util::codec::LengthDelimitedCodec`. The
prefix length and byte order are configurable and default to a 4‑byte
big‑endian header[^6].

```rust
let app = WireframeApp::new()?;
```

## Push Queues

Push queues buffer frames before they are written to a connection. Configure
them with capacities, rate limits, and an optional dead-letter queue:

```rust,no_run
use tokio::sync::mpsc;
use wireframe::push::PushQueues;

# async fn demo() {
let (dlq_tx, _dlq_rx) = mpsc::channel(8);
let (_queues, _handle) = PushQueues::<u8>::builder()
    .high_capacity(8)
    .low_capacity(8)
    .rate(Some(100))
    .dlq(Some(dlq_tx)) // frames drop if the DLQ is absent or full
    .build()
    .expect("failed to build PushQueues");
# drop((_queues, _handle));
# }
```

Disable throttling with the `unlimited` convenience:

```rust,no_run
use wireframe::push::PushQueues;
let (_queues, _handle) = PushQueues::<u8>::builder()
    .high_capacity(8)
    .low_capacity(8)
    .unlimited()
    .build()
    .expect("failed to build PushQueues");
```

## Connection Lifecycle

Protocol callbacks are consolidated under the `WireframeProtocol` trait,
replacing the individual `on_connection_setup`/`on_connection_teardown`
closures. The trait methods are synchronous so the trait remains object safe,
but callbacks can spawn asynchronous tasks when needed. A protocol
implementation registers hooks for connection setup, frame mutation and command
completion. The associated `ProtocolError` type is used by other parts of the
API, such as request handling.

```rust
pub trait WireframeProtocol: Send + Sync + 'static {
    type Frame: FrameLike;
    type ProtocolError;

    fn on_connection_setup(
        &self,
        handle: PushHandle<Self::Frame>,
        ctx: &mut ConnectionContext,
    );

    fn before_send(&self, frame: &mut Self::Frame, ctx: &mut ConnectionContext);

    fn on_command_end(&self, ctx: &mut ConnectionContext);
}

struct MySqlProtocolImpl;

impl WireframeProtocol for MySqlProtocolImpl {
    type Frame = Vec<u8>;
    type ProtocolError = ();

    fn on_connection_setup(
        &self,
        handle: PushHandle<Self::Frame>,
        _ctx: &mut ConnectionContext,
    ) {
        // Spawn an async task to send a heartbeat after setup
        tokio::spawn(async move {
            let _ = handle.push_high_priority(b"ping".to_vec()).await;
        });
    }

    fn before_send(&self, _frame: &mut Self::Frame, _ctx: &mut ConnectionContext) {}

    fn on_command_end(&self, _ctx: &mut ConnectionContext) {}
}

```

```rust
let app = WireframeApp::new().with_protocol(MySqlProtocolImpl);
```

## Session registry

The \[`SessionRegistry`\] stores weak references to \[`PushHandle`\]s for
active connections. Background tasks can look up a handle by \[`ConnectionId`\]
to send frames asynchronously without keeping the connection alive. Entries are
pruned on lookup and when calling `active_handles()`. `DashMap::retain` holds
per-bucket write locks while collecting, so heavy traffic may experience
contention. Invoke `prune()` from a maintenance task when only removal of dead
entries is required, without collecting handles.

```rust
use wireframe::{
    session::{ConnectionId, SessionRegistry},
    push::PushHandle,
    ConnectionContext,
};

let registry: SessionRegistry<MyFrame> = SessionRegistry::default();

// inside a `WireframeProtocol` implementation
fn on_connection_setup(&self, handle: PushHandle<MyFrame>, _ctx: &mut ConnectionContext) {
    let id = ConnectionId::new(42);
    registry.insert(id, &handle);
}
```

## Custom extractors

Extractors are types that implement `FromMessageRequest`. When a handler lists
an extractor as a parameter, `wireframe` automatically constructs it using the
incoming \[`MessageRequest`\] and remaining \[`Payload`\]. Built‑in extractors
like `Message<T>`, `SharedState<T>` and `ConnectionInfo` decode the payload,
access app state or expose peer information.

Custom extractors let you centralize parsing and validation logic that would
otherwise be duplicated across handlers. A session token parser, for example,
can verify the token before any route-specific code executes Design Guide: Data
Extraction and Type Safety[^7].

```rust
use wireframe::extractor::{ConnectionInfo, FromMessageRequest, MessageRequest, Payload};

pub struct SessionToken(String);

impl FromMessageRequest for SessionToken {
    type Error = std::convert::Infallible;

    fn from_message_request(
        _req: &MessageRequest,
        payload: &mut Payload<'_>,
    ) -> Result<Self, Self::Error> {
        let len = payload.as_ref()[0] as usize;
        let token = std::str::from_utf8(&payload.as_ref()[1..=len]).unwrap().to_string();
        payload.advance(1 + len);
        Ok(Self(token))
    }
}
```

Custom extractors integrate seamlessly with other parameters:

```rust
async fn handle_ping(token: SessionToken, info: ConnectionInfo) {
    println!("{} from {:?}", token.0, info.peer_addr());
}
```

## Middleware

Middleware allows inspecting or modifying requests and responses. The `from_fn`
helper builds middleware from an async function or closure:

```rust
use wireframe::middleware::from_fn;

let logging = from_fn(|req, next| async move {
    tracing::info!("received request: {:?}", req);
    let res = next.call(req).await?;
    tracing::info!("sending response: {:?}", res);
    Ok(res)
});
```

## Examples

Example programs are available in the `examples/` directory:

- `echo.rs` — minimal echo server using routing
- `ping_pong.rs` — showcases serialization and middleware in a ping/pong
  protocol. See [examples/ping_pong.md]examples/ping_pong.md for a detailed
  overview.
- [`packet_enum.rs`]examples/packet_enum.rs — shows packet type discrimination
  with a bincode enum and a frame containing container types like `HashMap` and
  `Vec`.

Run an example with Cargo:

```bash
cargo run --example echo
```

Try the echo server with netcat:

```bash
$ cargo run --example echo
# in another terminal
$ printf '\x00\x00\x00\x00\x01\x00\x00\x00' | nc 127.0.0.1 7878 | xxd
```

Try the ping‑pong server with netcat:

```bash
$ cargo run --example ping_pong
# in another terminal
$ printf '\x00\x00\x00\x08\x01\x00\x00\x00\x2a\x00\x00\x00' | nc 127.0.0.1 7878 | xxd
```

## Current limitations

Connection handling now processes frames and routes messages. Although the
server is still experimental, it now compiles in release mode for evaluation or
production use.

## Roadmap

Development priorities are tracked in [docs/roadmap.md](docs/roadmap.md). Key
tasks include building the Actix‑inspired API, implementing middleware and
extractor traits, and providing example applications[^8].

## Licence

Wireframe is distributed under the terms of the ISC licence. See
[LICENSE](LICENSE) for details.

[^1]: <docs/preamble-validator.md>
[^2]: <docs/rust-binary-router-library-design.md#L292-L344>
[^3]: <docs/rust-binary-router-library-design.md#L622-L710>
[^4]: <docs/rust-binary-router-library-design.md#L682-L710>
[^5]: <docs/rust-binary-router-library-design.md#L724-L730>
[^6]: <docs/rust-binary-router-library-design.md#L1082-L1123>
[^7]: <docs/rust-binary-router-library-design.md#53-data-extraction-and-type-safety>
[^8]: <docs/roadmap.md#L1-L24>