Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ metrics = { version = "0.24.2", optional = true }
thiserror = "2.0.16"
static_assertions = "1.1.0"
derive_more = { version = "2.0.1", features = ["display", "from"] }
socket2 = "0.6.0"

[dev-dependencies]
rstest = "0.26.1"
Expand Down
4 changes: 2 additions & 2 deletions docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,12 @@ logic.
## Phase 8: Wireframe client library foundation

This phase delivers a first-class client runtime that mirrors the server's
framing, serialisation, and lifecycle layers so both sides share the same
framing, serialisation, and lifecycle layers, so both sides share the same
behavioural guarantees.

- [ ] **Connection runtime:**

- [ ] Implement `WireframeClient` and its builder so callers can configure
- [x] Implement `WireframeClient` and its builder so callers can configure
serializers, codec settings (including `max_frame_length` parity), and
socket options before connecting.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Expand Down
58 changes: 53 additions & 5 deletions docs/users-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ as an `Arc` pointing to an async function that receives a packet reference and
returns `()`. The builder caches these registrations until `handle_connection`
constructs the middleware chain for an accepted stream.[^2]

```rust
```no_run
use std::sync::Arc;
use wireframe::app::{Envelope, Handler, WireframeApp};

Expand Down Expand Up @@ -99,12 +99,12 @@ async fn main() -> Result<(), ServerError> {
```

Route identifiers must be unique; the builder returns
`WireframeError::DuplicateRoute` when you try to register a handler twice,
`WireframeError::DuplicateRoute` when a handler is registered twice,
keeping the dispatch table unambiguous.[^2][^5] New applications default to the
bundled bincode serializer, a 1024-byte frame buffer, and a 100 ms read
timeout. Clamp these limits with `buffer_capacity` and `read_timeout_ms`, or
swap the serializer with `with_serializer` when you need a different encoding
strategy.[^3][^4]
swap the serializer with `with_serializer` when a different encoding strategy
is required.[^3][^4]

Once a stream is accepted—either from a manual accept loop or via
`WireframeServer`—`handle_connection(stream)` builds (or reuses) the middleware
Expand Down Expand Up @@ -157,7 +157,7 @@ layer evolves. The helper is fallible—`FragmentationError` surfaces encoding
failures or index overflows—so production code should bubble the error up or
log it rather than unwrapping.

```rust
```no_run
use std::num::NonZeroUsize;
use wireframe::fragment::Fragmenter;

Expand Down Expand Up @@ -378,6 +378,52 @@ the failure callback path.[^20]
worker tasks.[^20][^37][^38] `ServerError` surfaces bind and accept failures as
typed errors so callers can react appropriately.[^21]

## Client runtime

`WireframeClient` provides a first-class client runtime that mirrors the
server's framing and serialization layers, with a builder that configures the
serializer, codec settings, and socket options before connecting.[^44] Use
`ClientCodecConfig` to align `max_frame_length` with the server's
`buffer_capacity`, and apply `SocketOptions` when TCP tuning is required, such
as `TCP_NODELAY` or buffer size adjustments.

```rust
use std::{net::SocketAddr, time::Duration};

use wireframe::{
client::{ClientCodecConfig, SocketOptions},
WireframeClient,
};

#[derive(bincode::Encode, bincode::BorrowDecode)]
struct Login {
username: String,
}

#[derive(bincode::Encode, bincode::BorrowDecode, Debug, PartialEq)]
struct LoginAck {
ok: bool,
}

let addr: SocketAddr = "127.0.0.1:7878".parse().expect("valid socket address");
let codec = ClientCodecConfig::default().max_frame_length(2048);
let socket = SocketOptions::default()
.nodelay(true)
.keepalive(Some(Duration::from_secs(30)));

let mut client = WireframeClient::builder()
.codec_config(codec)
.socket_options(socket)
.connect(addr)
.await?;

let login = Login {
username: "guest".to_string(),
};
let ack: LoginAck = client.call(&login).await?;
assert!(ack.ok);
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

## Push queues and connection actors

Background work interacts with connections through `PushQueues`. The fluent
Expand Down Expand Up @@ -623,3 +669,5 @@ call these helpers to maintain consistent telemetry.[^6][^7][^31][^20]
[^41]: Implemented in `src/fragment/mod.rs` and supporting submodules.
[^42]: Exercised in `tests/features/fragment.feature`.
[^43]: Step definitions in `tests/steps/fragment_steps.rs`.
[^44]: Implemented in `src/client/runtime.rs`, `src/client/builder.rs`,
`src/client/config.rs`, and `src/client/error.rs`.
30 changes: 24 additions & 6 deletions docs/wireframe-client-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ implementation of a lightweight client without duplicating protocol code.
A new `WireframeClient` type manages a single connection to a server. It
mirrors `WireframeServer` but operates in the opposite direction:

- Connect to a `TcpStream`.
- Connect to a `TcpStream`, applying `SocketOptions` before the handshake.
- Optionally, send a preamble using the existing `Preamble` helpers.
- Encode outgoing messages using the selected `Serializer` and
`tokio_util::codec::LengthDelimitedCodec` (4‑byte big‑endian prefix by
default; configurable). Configure the codec’s `max_frame_length` on both the
inbound (decode) and outbound (encode) paths to match the server’s frame
capacity; otherwise, frames larger than the default 8 MiB will fail.
capacity; otherwise, frames larger than the configured limit will fail.
- Decode incoming frames into typed responses.
- Expose async `send` and `receive` operations.

Expand All @@ -36,9 +36,15 @@ mirrors `WireframeServer` but operates in the opposite direction:
A `WireframeClient::builder()` method configures the client:

```rust
use std::net::SocketAddr;

use wireframe::{BincodeSerializer, WireframeClient};

let addr: SocketAddr = "127.0.0.1:7878".parse()?;
let client = WireframeClient::builder()
.serializer(BincodeSerializer)
.connect("127.0.0.1:7878")
.max_frame_length(1024)
.connect(addr)
.await?;
```

Expand All @@ -53,13 +59,22 @@ message implementing `Message` and waits for the next response frame:

```rust
let request = Login { username: "guest".into() };
let response: LoginAck = client.call(request).await?;
let response: LoginAck = client.call(&request).await?;
```

Internally, this uses the `Serializer` to encode the request, sends it through
the length‑delimited codec, then waits for a frame, decodes it, and
deserializes the response type.

### Implementation decisions

- `connect` accepts a `SocketAddr` so the client can create a `TcpSocket` and
apply socket options before connecting.
- `ClientCodecConfig` captures the length prefix format and maximum frame
length, clamping the frame length to match server bounds (64 bytes to 16 MiB).
- The default `max_frame_length` is 1024 bytes to mirror the server builder’s
default buffer capacity.

### Connection lifecycle

Like the server, the client should expose hooks for connection setup and
Expand All @@ -71,13 +86,16 @@ share initialization logic.
```rust
#[tokio::main]
async fn main() -> std::io::Result<()> {
use std::net::SocketAddr;

let mut client = WireframeClient::builder()
.serializer(BincodeSerializer)
.connect("127.0.0.1:7878")
.max_frame_length(1024)
.connect("127.0.0.1:7878".parse::<SocketAddr>()?)
.await?;

let login = Login { username: "guest".into() };
let ack: LoginAck = client.call(login).await?;
let ack: LoginAck = client.call(&login).await?;
println!("logged in: {:?}", ack);
Ok(())
}
Expand Down
Loading
Loading