[WIP] Pings, Pongs & Streams
Some checks failed
CI / checks (push) Failing after 1m38s

This commit is contained in:
Alex Emmet 2026-07-14 00:14:53 +02:00
commit c148314742
17 changed files with 541 additions and 70 deletions

View file

@ -18,10 +18,14 @@ mtp = { path = "/path/to/mtp", features = ["client", "crypto"] }
```rust
use mtp::client::{ClientConfig, ClientTlsConfig};
use std::time::Duration;
let config = ClientConfig::new("https://host.example.com:4433")
.with_tls(ClientTlsConfig::SystemRoots)
.with_client_id(0);
.with_client_id(0)
.with_ping_interval(Duration::from_secs(5))
.with_max_missed_pings(3)
.with_ping_timestamp(true);
```
| Field | Type | Description |
@ -30,6 +34,9 @@ let config = ClientConfig::new("https://host.example.com:4433")
| `tls` | `ClientTlsConfig` | `SystemRoots` or `PinnedPem(pem_bytes)` |
| `client_id` | `u64` | Client identifier (ignored during `auth_register`) |
| `description` | `Option<String>` | Optional label sent during handshake (e.g. `"phone"`) |
| `ping_interval` | `Duration` | Interval between Ping frames; zero disables pings |
| `max_missed_pings` | `usize` | Unanswered Ping frames allowed before the connection closes |
| `ping_timestamp` | `bool` | Adds a `Timestamp` entry to each Ping frame |
| `auth_timeout` | `Duration` (crypto) | Authentication handshake timeout (default 30s) |
### TLS Certificate Handling
@ -74,6 +81,46 @@ pub struct MTPConnection {
- `description` -- the label sent during handshake (set via `ClientConfig::with_description`)
- `client_id` -- the confirmed/assigned client identifier (crypto only)
When `ping_interval` is non-zero, MTP sends Ping frames in the background and
consumes their Pong responses before application message handling. `get_ping()`
returns the round-trip duration of the latest matched Pong, or `None` until a
Pong arrives. A connection closes when the configured unanswered Ping limit is
reached.
### Ping-Pong
Ping/Pong is part of the protocol, not just a transport keepalive. Each Ping
frame is matched against a Pong with the same frame id, and the client uses the
response to update `get_ping()`. If the host does not answer within the
configured limit, the connection closes.
Enable it in `ClientConfig`, then inspect the latest round-trip time on the
connection. Pings start after the connection has been established; `None` is
normal until the first matching Pong arrives.
```rust
use mtp::client::{ClientConfig, MTPClient};
use std::time::Duration;
let config = ClientConfig::new("https://host.example.com:4433")
.with_client_id(42)
.with_ping_interval(Duration::from_secs(5))
.with_max_missed_pings(3)
.with_ping_timestamp(true);
let conn = MTPClient::connect(config).await?;
if let Some(round_trip) = conn.get_ping() {
println!("latest MTP round trip: {round_trip:?}");
}
```
The client consumes the Pong frames used by this loop, so they are not returned
by `conn.receiver.receive()`. Set `ping_interval` to `Duration::ZERO` (the
default) to disable protocol pings. `max_missed_pings` is the number of
outstanding Ping frames allowed before the client closes the connection; use a
host with automatic Pong responses, or provide an equivalent responder.
### Unauthenticated Connect
```rust

View file

@ -48,6 +48,7 @@ let config = HostConfig::new(
| `port` | `u16` | Listen port |
| `tls_fullchain` | `Vec<u8>` | PEM-encoded TLS certificate chain |
| `tls_key` | `Vec<u8>` | PEM-encoded TLS private key |
| `send_pongs` | `bool` | Sends a Pong for each received Ping (default `true`) |
| `authentication_policy` | `AuthenticationPolicy` (crypto) | `ForceAuthentication`, `AllowAuthentication`, or `Unauthenticated` |
| `host_keyring` | `Keyring` (crypto) | Host's signing and KEM keys |
| `get_existing_user` | `Fn(u64) -> Pin<Box<dyn Future<Output = Option<PublicKeyBundle>> + Send>> + Send + Sync` (crypto) | Async lookup callback for login |
@ -77,6 +78,35 @@ let config = HostConfig::new(ip, port, cert, key);
The host requires a TLS certificate. For development, generate a self-signed
certificate using `rcgen`. For production, use a CA-signed certificate.
### Ping-Pong
The host handles protocol Ping/Pong automatically unless you disable it with
`with_pongs(false)`. Enable the default responder explicitly when constructing
the host if you want to make the choice visible in application configuration:
```rust
let config = HostConfig::new(ip, port, cert, key)
.with_pongs(true);
```
For every received Ping, the responder sends a Pong with the same frame id and
copies the optional `Timestamp` data entry. Ping and Pong frames handled this
way are not delivered by `conn.receiver.receive()`. This lets native clients
use `ClientConfig::with_ping_interval` and `MTPConnection::get_ping()` without
adding application-level handlers.
Disable it only when the application needs to handle Ping frames itself:
```rust
let config = HostConfig::new(ip, port, cert, key)
.with_pongs(false);
```
With automatic responses disabled, Ping frames are delivered through the normal
receiver and the application is responsible for sending a compatible Pong (the
same frame id, and normally the Ping's `Timestamp`) if it wants clients to
continue their protocol ping loop.
## Accepting Connections
```rust

View file

@ -149,6 +149,39 @@ If hashes are omitted, the browser uses its normal TLS root store.
`maxMessageSize` caps inbound and outbound MTP frames before buffering/sending.
`authTimeoutMs` bounds connect/login/register promises at the SDK layer.
## Streams
The browser client uses one WebTransport session per `MTPClient` instance.
`send()`, `request()`, and `subscribe()` all operate over that session; the SDK
does not expose browser stream objects directly.
Use the normal message APIs to send and receive over that session:
```typescript
const client = await MTPClient.create({ url, hostPublicKey });
await client.connect();
const unsubscribe = client.subscribe("SomeType", (message) => {
console.log(message.data);
});
await client.send("SomeType", { value: "hello" });
unsubscribe();
```
Internally, each outbound MTP frame is written to a new WebTransport
unidirectional stream as a four-byte big-endian length followed by the frame,
then that stream is closed. Incoming frames are read from the session's
incoming unidirectional streams. The reader accepts both one-frame streams and
native peers that place several frames on a persistent stream, so browser and
native clients interoperate without stream configuration.
The SDK deliberately owns stream lifetime and framing. Do not create browser
streams for MTP frames yourself through the SDK. For direct generated bindings,
use `client.raw.client` or import `WasmClient` from `mtp/raw`; a `WasmClient`
still owns one active WebTransport session, so create another instance for an
independent connection.
## Sending, Requests, Subscriptions, And Pings
`send` accepts either a typed message or a prebuilt raw frame: