0.3.5-alpha.3
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 97
|
||||
# version: 98
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -25,7 +25,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.5-alpha.2.fix.1"
|
||||
version = "0.3.5-alpha.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/games"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 65 -->
|
||||
<!-- version: 66 -->
|
||||
|
||||
# games.sasedev
|
||||
|
||||
@@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale
|
||||
|
||||
Version stable de référence : `0.3.4`.
|
||||
|
||||
Version active : `0.3.5-alpha.2.fix.1`. `0.3.2` reste différée.
|
||||
Version active : `0.3.5-alpha.3`. `0.3.2` reste différée.
|
||||
|
||||
La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.2.fix.1` ferme le test d’établissement du backend WebTransport natif minimal : Quinn/HTTP/3, identité TLS locale ECDSA P-256 courte ou injectée, pin SHA-256 et établissement client/server loopback. Streams applicatifs, framing fiable et adaptation `RealtimeConnection` restent réservés à la tranche suivante.
|
||||
La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.3` ajoute au backend WebTransport natif le stream bidirectionnel fiable principal, un framing privé borné `u32` big-endian + payload et l'adaptation `RealtimeConnection`/sender/receiver, avec round-trip binaire ordonné et fermeture logique de base. Robustesse avancée, navigateur/WASM, smoke public et datagrams restent réservés aux tranches suivantes.
|
||||
|
||||
Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme.
|
||||
|
||||
@@ -45,4 +45,4 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna
|
||||
|
||||
## Diagnostics et tests
|
||||
|
||||
Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite, et `game-realtime-webtransport-lib`, backend WebTransport/QUIC candidat dont la première frontière native couvre TLS/pinning et établissement de session. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` fournit la preuve runtime localhost hors harness de test ; le smoke WebTransport public est planifié après le chemin fiable. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`.
|
||||
Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite, et `game-realtime-webtransport-lib`, backend WebTransport/QUIC candidat dont le chemin natif couvre désormais TLS/pinning, établissement de session et stream fiable principal adapté au contrat commun. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` fournit la preuve runtime localhost hors harness de test ; le smoke WebTransport public est planifié après la tranche de robustesse. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/README.md -->
|
||||
<!-- version: 1 -->
|
||||
<!-- version: 2 -->
|
||||
|
||||
# game-realtime-webtransport-lib
|
||||
|
||||
@@ -9,13 +9,18 @@ Backend WebTransport/QUIC candidat pour le realtime de `games.sasedev`.
|
||||
|
||||
La crate possède le transport WebTransport concret sans introduire de sémantique gameplay, room, joueur, tick ou snapshot. Son chemin natif repose sur `web-transport-quinn` et conserve les erreurs publiques dans `game-realtime-transport-lib`.
|
||||
|
||||
La première frontière disponible couvre :
|
||||
La frontière native disponible couvre désormais :
|
||||
|
||||
- configuration client HTTPS avec pin SHA-256 exact ;
|
||||
- identité serveur X.509 DER + clé privée PKCS#8 DER injectables ;
|
||||
- génération locale d'une identité self-signed ECDSA P-256 à validité courte pour `localhost`, IPv4 loopback et IPv6 loopback ;
|
||||
- bind UDP/QUIC sur adresse explicite ou port éphémère ;
|
||||
- établissement HTTP/3 WebTransport client/server natif ;
|
||||
- sélection d'un unique stream bidirectionnel fiable comme chemin realtime principal ;
|
||||
- framing privé `u32` big-endian + payload binaire ;
|
||||
- borne POC de 1 MiB vérifiée avant allocation côté réception et avant écriture côté émission ;
|
||||
- adaptation `RealtimeConnection` / `RealtimeSender` / `RealtimeReceiver` ;
|
||||
- fermeture propre du chemin logique par FIN du stream primaire ;
|
||||
- tracing sous `games::realtime::webtransport`.
|
||||
|
||||
## TLS de développement
|
||||
@@ -26,10 +31,42 @@ Une identité préexistante peut être injectée en DER avec `WebTransportServer
|
||||
|
||||
Aucune option de désactivation globale de la vérification TLS n'est exposée.
|
||||
|
||||
## Stream fiable principal
|
||||
|
||||
Une session WebTransport établie n'est pas encore le contrat realtime lui-même. Le client appelle `WebTransportSession::open_primary_connection()` ; le serveur appelle `WebTransportSession::accept_primary_connection()`.
|
||||
|
||||
Le chemin logique devient ensuite :
|
||||
|
||||
```text
|
||||
one WebTransport session
|
||||
-> one primary bidirectional reliable stream
|
||||
-> u32 big-endian payload length
|
||||
-> payload bytes
|
||||
```
|
||||
|
||||
`open_primary_connection()` écrit déjà l'en-tête de stream WebTransport requis par HTTP/3 avant de retourner. Le pair peut donc terminer `accept_primary_connection()` avant l'envoi de la première frame applicative ; aucun préambule propre à games.sasedev n'est nécessaire.
|
||||
|
||||
`RealtimeConnection::split()` conserve la session WebTransport dans les deux moitiés afin que la session ne soit pas fermée au moment où l'objet connexion est consommé.
|
||||
|
||||
## Limite de frame POC
|
||||
|
||||
La borne actuelle du framing fiable est volontairement interne à cette tranche : 1 MiB par message. Elle empêche une longueur `u32` hostile de provoquer une allocation arbitraire et rejette aussi l'émission hors limite avec `TransportErrorKind::MessageTooLarge`.
|
||||
|
||||
Cette valeur n'est pas encore une configuration produit. La tranche de robustesse suivante doit décider si la limite devient configurable avec les deadlines, la backpressure, les resets, l'abort/cancellation et le mapping d'erreurs détaillé.
|
||||
|
||||
## Frontières actuelles
|
||||
|
||||
Cette crate n'adapte pas encore une session vers `RealtimeConnection`. Elle n'ouvre pas encore le stream bidirectionnel applicatif principal et ne définit donc ni framing message, ni limites de payload, ni deadlines applicatives, ni datagram API commune.
|
||||
La crate ne possède toujours pas :
|
||||
|
||||
Ces responsabilités doivent rester séparées de la seule preuve d'établissement natif afin que l'introduction de QUIC/TLS demeure testable indépendamment du futur framing fiable.
|
||||
- d'API datagram transport-neutral ;
|
||||
- de deadlines applicatives WebTransport ;
|
||||
- de politique de backpressure explicite ;
|
||||
- de reset/abort/cancellation produit ;
|
||||
- de mapping fin de toutes les erreurs Quinn/WebTransport ;
|
||||
- de chemin navigateur/WASM ;
|
||||
- de smoke executable public WebTransport ;
|
||||
- de benchmark WebSocket/WebTransport.
|
||||
|
||||
Le chemin natif s’exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Le chemin navigateur/WASM est distinct : aucun `cfg` WASM ni dépendance navigateur n'est requis par le backend natif actuel.
|
||||
Ces responsabilités restent réservées aux tranches suivantes du plan `0.3.5`.
|
||||
|
||||
Le chemin natif s'exécute sous un runtime Tokio fourni par le consommateur ; la crate ne crée ni runtime ni thread privé. Le chemin navigateur/WASM est distinct : aucun `cfg` WASM ni dépendance navigateur n'est requis par le backend natif actuel.
|
||||
|
||||
89
crates/common/game-realtime-webtransport-lib/USAGE.md
Normal file
89
crates/common/game-realtime-webtransport-lib/USAGE.md
Normal file
@@ -0,0 +1,89 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/USAGE.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Utilisation de game-realtime-webtransport-lib
|
||||
|
||||
Ce guide décrit le chemin natif fiable actuellement exposé par `game-realtime-webtransport-lib`. Il ne décrit ni gameplay, ni protocole wire métier, ni datagrams.
|
||||
|
||||
## Serveur natif
|
||||
|
||||
Créer d'abord l'identité TLS et le listener :
|
||||
|
||||
```rust
|
||||
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let config = game_realtime_webtransport_lib::WebTransportServerConfig::new(
|
||||
std::net::SocketAddr::from(([127, 0, 0, 1], 4433)),
|
||||
identity,
|
||||
);
|
||||
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let session = match listener.accept().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let connection = match session.accept_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
`open_primary_connection()` écrit l'en-tête WebTransport requis pour identifier le stream avant de retourner. Le serveur peut donc attendre `accept_primary_connection()` puis commencer les échanges applicatifs ; aucune frame artificielle n'est nécessaire pour rendre le stream visible.
|
||||
|
||||
## Client natif
|
||||
|
||||
Le client doit connaître le SHA-256 exact du certificat serveur :
|
||||
|
||||
```rust
|
||||
let config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(
|
||||
"https://127.0.0.1:4433/game",
|
||||
certificate_hash,
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let session = match game_realtime_webtransport_lib::connect(&config).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let connection = match session.open_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
```
|
||||
|
||||
Le pinning est obligatoire dans cette API native ; il n'existe pas de variante qui désactive globalement la vérification TLS.
|
||||
|
||||
## Contrat realtime
|
||||
|
||||
Une fois le stream primaire sélectionné, utiliser uniquement les traits de `game-realtime-transport-lib` :
|
||||
|
||||
```rust
|
||||
let (mut sender, mut receiver) = game_realtime_transport_lib::RealtimeConnection::split(connection);
|
||||
|
||||
let message = game_realtime_transport_lib::TransportMessage::new(vec![1, 2, 3, 4]);
|
||||
if let Err(error) = game_realtime_transport_lib::RealtimeSender::send(&mut sender, message).await {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(&mut receiver).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut sender).await {
|
||||
return Err(error);
|
||||
}
|
||||
```
|
||||
|
||||
Le backend encode chaque `TransportMessage` sous la forme `u32` big-endian + payload. Le consommateur ne doit pas reproduire ce framing lui-même.
|
||||
|
||||
## Fermeture actuelle
|
||||
|
||||
`RealtimeSender::close()` termine proprement la direction d'émission du stream primaire. Le pair observe ensuite `TransportReceive::Closed` lorsqu'il atteint le FIN après les messages déjà écrits.
|
||||
|
||||
La fermeture de session complète, les resets, les aborts, les timeouts et les cas d'annulation appartiennent à la tranche de robustesse suivante et ne doivent pas être simulés par le consommateur.
|
||||
@@ -1,25 +1,31 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/lib.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Native Quinn-backed WebTransport establishment for the games.sasedev realtime transport POC.
|
||||
//! Native WebTransport/QUIC backend candidate for the transport-neutral realtime contract.
|
||||
|
||||
mod webtransport;
|
||||
|
||||
/// Re-export of one SHA-256 certificate fingerprint accepted by the native WebTransport client.
|
||||
/// Re-export of the pinned SHA-256 certificate fingerprint used by the native client.
|
||||
pub use self::webtransport::WebTransportCertificateHash;
|
||||
/// Re-export of native client establishment configuration.
|
||||
/// Re-export of native WebTransport client configuration.
|
||||
pub use self::webtransport::WebTransportClientConfig;
|
||||
/// Re-export of a bound native WebTransport server listener.
|
||||
/// Re-export of an established WebTransport connection adapted to the transport-neutral realtime contract.
|
||||
pub use self::webtransport::WebTransportConnection;
|
||||
/// Re-export of the bound native WebTransport listener.
|
||||
pub use self::webtransport::WebTransportListener;
|
||||
/// Re-export of native WebTransport server establishment configuration.
|
||||
/// Re-export of the receive half of the primary reliable WebTransport stream.
|
||||
pub use self::webtransport::WebTransportReceiver;
|
||||
/// Re-export of the send half of the primary reliable WebTransport stream.
|
||||
pub use self::webtransport::WebTransportSender;
|
||||
/// Re-export of native WebTransport server configuration.
|
||||
pub use self::webtransport::WebTransportServerConfig;
|
||||
/// Re-export of a generated or injected WebTransport server identity.
|
||||
/// Re-export of native WebTransport server TLS identity material.
|
||||
pub use self::webtransport::WebTransportServerIdentity;
|
||||
/// Re-export of an established native WebTransport session.
|
||||
pub use self::webtransport::WebTransportSession;
|
||||
/// Re-export of the native WebTransport client session constructor.
|
||||
/// Re-export of the native WebTransport client establishment function.
|
||||
pub use self::webtransport::connect;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/webtransport.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
const CERTIFICATE_HASH_SIZE: usize = 32;
|
||||
const LOCAL_CERTIFICATE_CLOCK_SKEW: std::time::Duration = std::time::Duration::from_secs(60);
|
||||
const LOCAL_CERTIFICATE_VALIDITY: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 60 * 60);
|
||||
const PRIMARY_FRAME_HEADER_SIZE: usize = 4;
|
||||
const PRIMARY_FRAME_MAX_PAYLOAD_SIZE: usize = 1024 * 1024;
|
||||
const TRACING_TARGET: &str = "games::realtime::webtransport";
|
||||
|
||||
/// SHA-256 fingerprint of one certificate accepted by the native WebTransport client.
|
||||
@@ -151,7 +153,7 @@ impl WebTransportServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Established native WebTransport session before application-stream adaptation.
|
||||
/// Established native WebTransport session before or while the single primary application stream is selected.
|
||||
pub struct WebTransportSession {
|
||||
inner: web_transport_quinn::Session,
|
||||
}
|
||||
@@ -161,6 +163,37 @@ impl WebTransportSession {
|
||||
return Self { inner };
|
||||
}
|
||||
|
||||
/// Accepts the peer-created primary bidirectional stream and adapts it to the transport-neutral realtime contract.
|
||||
///
|
||||
/// The native WebTransport wrapper writes the required stream/session header while opening the stream, so the peer can
|
||||
/// accept it before the first application frame is sent.
|
||||
pub async fn accept_primary_connection(self) -> Result<WebTransportConnection, game_realtime_transport_lib::TransportError> {
|
||||
let (sender, receiver) = match self.inner.accept_bi().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "WebTransport primary bidirectional stream accept failed");
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::debug!(target: TRACING_TARGET, peer = %self.inner.remote_address(), "WebTransport primary bidirectional stream accepted");
|
||||
return Ok(WebTransportConnection::new(self.inner, sender, receiver));
|
||||
}
|
||||
|
||||
/// Opens the single primary bidirectional stream and adapts it to the transport-neutral realtime contract.
|
||||
pub async fn open_primary_connection(self) -> Result<WebTransportConnection, game_realtime_transport_lib::TransportError> {
|
||||
let (sender, receiver) = match self.inner.open_bi().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "WebTransport primary bidirectional stream open failed");
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::debug!(target: TRACING_TARGET, peer = %self.inner.remote_address(), "WebTransport primary bidirectional stream opened");
|
||||
return Ok(WebTransportConnection::new(self.inner, sender, receiver));
|
||||
}
|
||||
|
||||
/// Returns the remote UDP socket address backing the established QUIC connection.
|
||||
#[must_use]
|
||||
pub fn remote_addr(&self) -> std::net::SocketAddr {
|
||||
@@ -177,6 +210,121 @@ impl WebTransportSession {
|
||||
}
|
||||
}
|
||||
|
||||
/// Established WebTransport realtime connection carried by one primary reliable bidirectional stream.
|
||||
pub struct WebTransportConnection {
|
||||
receiver: web_transport_quinn::RecvStream,
|
||||
sender: web_transport_quinn::SendStream,
|
||||
session: web_transport_quinn::Session,
|
||||
}
|
||||
|
||||
impl WebTransportConnection {
|
||||
fn new(session: web_transport_quinn::Session, sender: web_transport_quinn::SendStream, receiver: web_transport_quinn::RecvStream) -> Self {
|
||||
return Self { receiver, sender, session };
|
||||
}
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeConnection for WebTransportConnection {
|
||||
type Receiver = crate::WebTransportReceiver;
|
||||
type Sender = crate::WebTransportSender;
|
||||
|
||||
fn split(self) -> (Self::Sender, Self::Receiver) {
|
||||
let receiver_session = self.session.clone();
|
||||
return (
|
||||
crate::WebTransportSender { inner: self.sender, _session: self.session },
|
||||
crate::WebTransportReceiver { inner: self.receiver, _session: receiver_session },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive half of the primary reliable WebTransport stream.
|
||||
pub struct WebTransportReceiver {
|
||||
inner: web_transport_quinn::RecvStream,
|
||||
_session: web_transport_quinn::Session,
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeReceiver for WebTransportReceiver {
|
||||
type ReceiveFuture<'a>
|
||||
= std::pin::Pin<
|
||||
Box<dyn core::future::Future<Output = Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError>> + 'a>,
|
||||
>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn receive(&mut self) -> Self::ReceiveFuture<'_> {
|
||||
return Box::pin(async move {
|
||||
let payload_len = match read_frame_payload_len(&mut self.inner).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
tracing::debug!(target: TRACING_TARGET, "remote WebTransport primary stream closed cleanly");
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
|
||||
},
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let payload = match read_frame_payload(&mut self.inner, payload_len).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload.len(), "framed WebTransport payload received");
|
||||
return Ok(game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(payload)));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Send half of the primary reliable WebTransport stream.
|
||||
pub struct WebTransportSender {
|
||||
inner: web_transport_quinn::SendStream,
|
||||
_session: web_transport_quinn::Session,
|
||||
}
|
||||
|
||||
impl game_realtime_transport_lib::RealtimeSender for WebTransportSender {
|
||||
type CloseFuture<'a>
|
||||
= std::pin::Pin<Box<dyn core::future::Future<Output = Result<(), game_realtime_transport_lib::TransportError>> + 'a>>
|
||||
where
|
||||
Self: 'a;
|
||||
type SendFuture<'a>
|
||||
= std::pin::Pin<Box<dyn core::future::Future<Output = Result<(), game_realtime_transport_lib::TransportError>> + 'a>>
|
||||
where
|
||||
Self: 'a;
|
||||
|
||||
fn close(&mut self) -> Self::CloseFuture<'_> {
|
||||
return Box::pin(async move {
|
||||
return match self.inner.finish() {
|
||||
Ok(()) => {
|
||||
tracing::debug!(target: TRACING_TARGET, "local WebTransport primary stream close initiated");
|
||||
Ok(())
|
||||
},
|
||||
Err(error) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Closed, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "WebTransport primary stream close failed");
|
||||
Err(mapped)
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
fn send(&mut self, message: game_realtime_transport_lib::TransportMessage) -> Self::SendFuture<'_> {
|
||||
return Box::pin(async move {
|
||||
let payload_len = message.len();
|
||||
let frame_header = match frame_header(payload_len) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
if let Err(error) = self.inner.write_all(&frame_header).await {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, detail = mapped.detail(), "WebTransport frame header send failed");
|
||||
return Err(mapped);
|
||||
}
|
||||
if let Err(error) = self.inner.write_all(message.as_bytes()).await {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, detail = mapped.detail(), "WebTransport frame payload send failed");
|
||||
return Err(mapped);
|
||||
}
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "framed WebTransport payload sent");
|
||||
return Ok(());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Bound native WebTransport server endpoint that accepts HTTP/3 WebTransport sessions.
|
||||
pub struct WebTransportListener {
|
||||
server: web_transport_quinn::Server,
|
||||
@@ -270,10 +418,75 @@ fn certificate_hash(certificate_der: &[u8]) -> Result<WebTransportCertificateHas
|
||||
return Ok(WebTransportCertificateHash::from_sha256(bytes));
|
||||
}
|
||||
|
||||
fn frame_header(payload_len: usize) -> Result<[u8; PRIMARY_FRAME_HEADER_SIZE], game_realtime_transport_lib::TransportError> {
|
||||
if payload_len > PRIMARY_FRAME_MAX_PAYLOAD_SIZE {
|
||||
return Err(message_too_large(payload_len));
|
||||
}
|
||||
let payload_len = match u32::try_from(payload_len) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Err(message_too_large(payload_len)),
|
||||
};
|
||||
return Ok(payload_len.to_be_bytes());
|
||||
}
|
||||
|
||||
fn invalid_configuration(detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration, detail);
|
||||
}
|
||||
|
||||
fn message_too_large(payload_len: usize) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(
|
||||
game_realtime_transport_lib::TransportErrorKind::MessageTooLarge,
|
||||
format!("WebTransport primary frame payload size {payload_len} exceeds {PRIMARY_FRAME_MAX_PAYLOAD_SIZE} bytes"),
|
||||
);
|
||||
}
|
||||
|
||||
async fn read_frame_payload(stream: &mut web_transport_quinn::RecvStream, payload_len: usize) -> Result<Vec<u8>, game_realtime_transport_lib::TransportError> {
|
||||
let mut payload = vec![0_u8; payload_len];
|
||||
let mut offset = 0_usize;
|
||||
while offset < payload_len {
|
||||
let read = match stream.read(&mut payload[offset..]).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => return Err(protocol_error("WebTransport primary stream closed in the middle of a frame payload")),
|
||||
Err(error) => return Err(protocol_error(error.to_string())),
|
||||
};
|
||||
if read == 0 {
|
||||
return Err(protocol_error("WebTransport primary stream returned an empty read in the middle of a frame payload"));
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
return Ok(payload);
|
||||
}
|
||||
|
||||
async fn read_frame_payload_len(stream: &mut web_transport_quinn::RecvStream) -> Result<Option<usize>, game_realtime_transport_lib::TransportError> {
|
||||
let mut header = [0_u8; PRIMARY_FRAME_HEADER_SIZE];
|
||||
let mut offset = 0_usize;
|
||||
while offset < PRIMARY_FRAME_HEADER_SIZE {
|
||||
let read = match stream.read(&mut header[offset..]).await {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => {
|
||||
if offset == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
return Err(protocol_error("WebTransport primary stream closed in the middle of a frame header"));
|
||||
},
|
||||
Err(error) => return Err(protocol_error(error.to_string())),
|
||||
};
|
||||
if read == 0 {
|
||||
return Err(protocol_error("WebTransport primary stream returned an empty read in the middle of a frame header"));
|
||||
}
|
||||
offset += read;
|
||||
}
|
||||
let payload_len = u32::from_be_bytes(header) as usize;
|
||||
if payload_len > PRIMARY_FRAME_MAX_PAYLOAD_SIZE {
|
||||
return Err(message_too_large(payload_len));
|
||||
}
|
||||
return Ok(Some(payload_len));
|
||||
}
|
||||
|
||||
fn protocol_error(detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, detail);
|
||||
}
|
||||
|
||||
fn transport_error(kind: game_realtime_transport_lib::TransportErrorKind, detail: impl Into<String>) -> game_realtime_transport_lib::TransportError {
|
||||
return game_realtime_transport_lib::TransportError::new(kind, detail);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/tests/realtime_connection.rs
|
||||
// version: 1
|
||||
|
||||
//! Deterministic loopback proof for the primary reliable WebTransport stream and transport-neutral framing contract.
|
||||
|
||||
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn primary_stream_round_trip_is_binary_ordered_and_closes_cleanly() {
|
||||
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
let certificate_hash = identity.certificate_hash().clone();
|
||||
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(std::net::SocketAddr::from(([127, 0, 0, 1], 0)), identity);
|
||||
let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport listener bind failed: {error}"),
|
||||
};
|
||||
let endpoint = format!("https://{}/realtime", listener.local_addr());
|
||||
let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("WebTransport client configuration failed: {error}"),
|
||||
};
|
||||
let sessions = tokio::time::timeout(TEST_TIMEOUT, async {
|
||||
return tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(&client_config));
|
||||
})
|
||||
.await;
|
||||
let (server_session, client_session) = match sessions {
|
||||
Ok((Ok(server), Ok(client))) => (server, client),
|
||||
Ok((Err(error), _)) => panic!("WebTransport server establishment failed: {error}"),
|
||||
Ok((_, Err(error))) => panic!("WebTransport client establishment failed: {error}"),
|
||||
Err(_) => panic!("WebTransport loopback establishment timed out"),
|
||||
};
|
||||
let client_connection = match client_session.open_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("client primary stream open failed: {error}"),
|
||||
};
|
||||
let (mut client_sender, mut client_receiver) = game_realtime_transport_lib::RealtimeConnection::split(client_connection);
|
||||
let server_connection = match tokio::time::timeout(TEST_TIMEOUT, server_session.accept_primary_connection()).await {
|
||||
Ok(Ok(connection)) => connection,
|
||||
Ok(Err(error)) => panic!("server primary stream accept failed: {error}"),
|
||||
Err(_) => panic!("server primary stream accept timed out"),
|
||||
};
|
||||
let (mut server_sender, mut server_receiver) = game_realtime_transport_lib::RealtimeConnection::split(server_connection);
|
||||
let first_payload = Vec::new();
|
||||
send_payload(&mut client_sender, first_payload.clone(), "client first send").await;
|
||||
assert_received_payload(&mut server_receiver, first_payload.clone(), "server first receive").await;
|
||||
send_payload(&mut server_sender, first_payload.clone(), "server first echo").await;
|
||||
assert_received_payload(&mut client_receiver, first_payload, "client first echo receive").await;
|
||||
let remaining_payloads = [vec![0x00, 0x7f, 0x80, 0xff], b"third-message".to_vec()];
|
||||
for payload in remaining_payloads {
|
||||
send_payload(&mut client_sender, payload.clone(), "client ordered send").await;
|
||||
assert_received_payload(&mut server_receiver, payload.clone(), "server ordered receive").await;
|
||||
send_payload(&mut server_sender, payload.clone(), "server ordered echo").await;
|
||||
assert_received_payload(&mut client_receiver, payload, "client ordered echo receive").await;
|
||||
}
|
||||
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut client_sender).await {
|
||||
panic!("client sender close failed: {error}");
|
||||
}
|
||||
assert_closed(&mut server_receiver, "server remote close").await;
|
||||
if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut server_sender).await {
|
||||
panic!("server sender close failed: {error}");
|
||||
}
|
||||
assert_closed(&mut client_receiver, "client remote close").await;
|
||||
}
|
||||
|
||||
async fn assert_closed(receiver: &mut game_realtime_webtransport_lib::WebTransportReceiver, label: &str) {
|
||||
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(receiver).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("{label} failed: {error}"),
|
||||
};
|
||||
assert_eq!(received, game_realtime_transport_lib::TransportReceive::Closed);
|
||||
}
|
||||
|
||||
async fn assert_received_payload(receiver: &mut game_realtime_webtransport_lib::WebTransportReceiver, payload: Vec<u8>, label: &str) {
|
||||
let received = match game_realtime_transport_lib::RealtimeReceiver::receive(receiver).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("{label} failed: {error}"),
|
||||
};
|
||||
assert_eq!(received, game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(payload)));
|
||||
}
|
||||
|
||||
async fn send_payload(sender: &mut game_realtime_webtransport_lib::WebTransportSender, payload: Vec<u8>, label: &str) {
|
||||
let message = game_realtime_transport_lib::TransportMessage::new(payload);
|
||||
if let Err(error) = game_realtime_transport_lib::RealtimeSender::send(sender, message).await {
|
||||
panic!("{label} failed: {error}");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
#[test]
|
||||
fn certificate_hash_preserves_exact_sha256_bytes() {
|
||||
@@ -25,6 +25,29 @@ fn client_config_accepts_https_and_rejects_non_secure_schemes() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_header_is_big_endian_and_payload_bound_is_enforced() {
|
||||
let header = match super::frame_header(0x00_01_02_03) {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("valid frame header rejected: {error}"),
|
||||
};
|
||||
assert_eq!(header, [0x00, 0x01, 0x02, 0x03]);
|
||||
let oversized = super::frame_header(super::PRIMARY_FRAME_MAX_PAYLOAD_SIZE + 1);
|
||||
match oversized {
|
||||
Ok(_) => panic!("oversized WebTransport frame unexpectedly accepted"),
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::MessageTooLarge),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_loopback_identity_has_sha256_fingerprint() {
|
||||
let identity = match super::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injected_identity_rejects_empty_certificate_or_key() {
|
||||
let missing_certificate = super::WebTransportServerIdentity::from_pkcs8_der(Vec::new(), vec![1]);
|
||||
@@ -38,12 +61,3 @@ fn injected_identity_rejects_empty_certificate_or_key() {
|
||||
Err(error) => assert_eq!(error.kind(), game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_loopback_identity_has_sha256_fingerprint() {
|
||||
let identity = match super::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => panic!("loopback identity generation failed: {error}"),
|
||||
};
|
||||
assert_eq!(identity.certificate_hash().as_bytes().len(), super::CERTIFICATE_HASH_SIZE);
|
||||
}
|
||||
|
||||
184
deltas/0.3.5/alpha.3.md
Normal file
184
deltas/0.3.5/alpha.3.md
Normal file
@@ -0,0 +1,184 @@
|
||||
<!-- file: deltas/0.3.5/alpha.3.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.3.5-alpha.3
|
||||
|
||||
## Base requise
|
||||
|
||||
`0.3.5-alpha.2.fix.1`, validée par l'utilisateur le 2026-09-21 avec fmt, audits, check workspace, Clippy strict et les six tests de `game-realtime-webtransport-lib` entièrement verts.
|
||||
|
||||
La validation réellement fournie est conservée dans `history/0.3.5/alpha.2.fix.1.md`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Fermer la première adaptation fiable WebTransport vers `game-realtime-transport-lib` sans introduire encore la robustesse/lifecycle avancés, le navigateur/WASM, les datagrams ou le smoke public.
|
||||
|
||||
La version passe à :
|
||||
|
||||
```text
|
||||
0.3.5-alpha.3
|
||||
```
|
||||
|
||||
## Stream primaire
|
||||
|
||||
Une `WebTransportSession` peut désormais être consommée de deux façons :
|
||||
|
||||
```text
|
||||
client -> open_primary_connection()
|
||||
server -> accept_primary_connection()
|
||||
```
|
||||
|
||||
Chaque chemin sélectionne exactement un stream bidirectionnel fiable et retourne `WebTransportConnection`.
|
||||
|
||||
Le backend `web-transport-quinn` écrit lui-même l'en-tête WebTransport nécessaire pendant `open_bi()`. Le serveur peut donc accepter le stream avant toute frame applicative. Aucun préambule games.sasedev supplémentaire n'est ajouté : après l'en-tête protocolaire géré par la dépendance, le premier octet applicatif appartient directement au framing prévu.
|
||||
|
||||
## Framing fiable privé
|
||||
|
||||
Le framing du stream primaire est :
|
||||
|
||||
```text
|
||||
u32 big-endian payload length
|
||||
payload bytes
|
||||
```
|
||||
|
||||
Il reste privé à `game-realtime-webtransport-lib` et n'est pas un wire codec gameplay.
|
||||
|
||||
La borne POC actuelle est de 1 MiB par message. Elle est vérifiée :
|
||||
|
||||
- avant écriture côté sender ;
|
||||
- immédiatement après décodage des quatre octets de longueur et avant toute allocation côté receiver.
|
||||
|
||||
Une longueur hors limite produit `TransportErrorKind::MessageTooLarge`.
|
||||
|
||||
La limite n'est pas encore exposée comme configuration produit. `alpha.4` possède cette décision avec les autres limites et deadlines.
|
||||
|
||||
## Contrat commun
|
||||
|
||||
`WebTransportConnection` implémente `RealtimeConnection` et produit :
|
||||
|
||||
```text
|
||||
WebTransportSender
|
||||
WebTransportReceiver
|
||||
```
|
||||
|
||||
Les deux moitiés conservent chacune une référence à la session WebTransport. Le `split()` ne ferme donc pas accidentellement la session au moment où l'objet connexion est consommé.
|
||||
|
||||
`WebTransportSender::send(...)` écrit le header puis le payload sur le stream fiable et respecte la backpressure naturelle de QUIC pendant les écritures asynchrones.
|
||||
|
||||
`WebTransportReceiver::receive(...)` reconstruit exactement un `TransportMessage`, y compris les payloads vides et binaires non UTF-8.
|
||||
|
||||
`RealtimeSender::close()` termine proprement la direction locale du stream primaire. Lorsque le pair a consommé les messages précédents puis atteint le FIN, `RealtimeReceiver::receive()` retourne `TransportReceive::Closed`.
|
||||
|
||||
La fermeture complète de session, reset, abort, cancellation, deadlines et mapping fin des erreurs restent hors de cette tranche.
|
||||
|
||||
## API et documentation
|
||||
|
||||
`src/lib.rs` réexporte les nouveaux types publics conformément aux règles du workspace.
|
||||
|
||||
Le README local est réconcilié avec le stream primaire et ses frontières. Un `USAGE.md` durable est ajouté car l'ordre session -> stream primaire -> `RealtimeConnection` et la contrainte d'accept serveur nécessitent désormais un guide d'utilisation distinct du delta.
|
||||
|
||||
Le plan `005` enregistre les décisions réellement matérialisées sans modifier le forecast de `alpha.4+`.
|
||||
|
||||
`ROADMAP.md` et `CHANGELOG.md` restent inchangés : cette alpha ne modifie ni la mission macro de `0.3.5` ni une livraison stable.
|
||||
|
||||
## Tests
|
||||
|
||||
Le test unitaire du backend ajoute la preuve que :
|
||||
|
||||
- la longueur est encodée en `u32` big-endian ;
|
||||
- la borne de frame refuse une taille supérieure à 1 MiB avec `MessageTooLarge`.
|
||||
|
||||
Le nouveau test d'intégration `tests/realtime_connection.rs` prouve en loopback :
|
||||
|
||||
- ouverture du stream primaire côté client ;
|
||||
- ouverture cliente puis accept du stream primaire côté serveur avant la première frame applicative ;
|
||||
- adaptation `RealtimeConnection` ;
|
||||
- payload vide ;
|
||||
- payload binaire non UTF-8 ;
|
||||
- ordre de plusieurs messages ;
|
||||
- echo bidirectionnel ;
|
||||
- FIN client observé comme `TransportReceive::Closed` côté serveur ;
|
||||
- FIN serveur observé comme `TransportReceive::Closed` côté client.
|
||||
|
||||
Les sessions restent vivantes pendant toute la preuve afin que le test ne confonde pas FIN du stream logique et drop de session.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
README.md
|
||||
crates/common/game-realtime-webtransport-lib/README.md
|
||||
crates/common/game-realtime-webtransport-lib/src/lib.rs
|
||||
crates/common/game-realtime-webtransport-lib/src/webtransport.rs
|
||||
crates/common/game-realtime-webtransport-lib/unit_tests/webtransport.rs
|
||||
docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md
|
||||
```
|
||||
|
||||
Nouveaux fichiers :
|
||||
|
||||
```text
|
||||
crates/common/game-realtime-webtransport-lib/USAGE.md
|
||||
crates/common/game-realtime-webtransport-lib/tests/realtime_connection.rs
|
||||
history/0.3.5/alpha.2.fix.1.md
|
||||
deltas/0.3.5/alpha.3.md
|
||||
```
|
||||
|
||||
Aucune dépendance Cargo n'est ajoutée ou modifiée dans cette tranche.
|
||||
|
||||
## Validation exécutée dans l'environnement de génération
|
||||
|
||||
L'environnement de génération ne possède pas de toolchain Rust. Aucun `cargo fmt`, `cargo check`, Clippy ou test n'est donc attribué à cette livraison.
|
||||
|
||||
Les audits statiques disponibles ont été exécutés sur le candidat final :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
games.sasedev workspace audit: clean
|
||||
Markdown table audit: clean (5 table(s), 273 file(s))
|
||||
Distribution layout audit: clean (50 required path(s), 8 forbidden path(s) absent)
|
||||
```
|
||||
|
||||
Le compteur de fichiers Markdown correspond à la reconstruction de travail issue de l'archive taggée et des deltas fournis. Il n'est pas utilisé comme invariant contre le checkout utilisateur, qui contient davantage de fichiers suivis localement.
|
||||
|
||||
## Validation utilisateur demandée
|
||||
|
||||
Cette tranche modifie le backend Rust et ajoute un test d'intégration. La gate ciblée est :
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo fmt --all -- --check
|
||||
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history
|
||||
python3 scripts/audit_distribution_layout.py
|
||||
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
|
||||
cargo test -p game-realtime-transport-lib --all-targets --all-features
|
||||
cargo test -p game-realtime-webtransport-lib --all-targets --all-features
|
||||
|
||||
cargo tree -i game-realtime-webtransport-lib --workspace --edges normal
|
||||
```
|
||||
|
||||
Le contrat commun est retesté parce que `alpha.3` en devient un nouvel implémenteur, même si sa crate n'est pas modifiée.
|
||||
|
||||
Le `cargo tree` inverse vérifie qu'aucune crate moteur ou gameplay n'a acquis de dépendance vers le backend concret. Les arbres complets `web-transport-quinn` de `alpha.2` ne sont pas répétés puisque le graphe de dépendances n'a pas changé.
|
||||
|
||||
Aucun smoke executable n'est encore attendu : le smoke natif public reste `alpha.5`, après la tranche de robustesse `alpha.4`.
|
||||
|
||||
## Suite après validation
|
||||
|
||||
Si la gate est propre, ouvrir `0.3.5-alpha.4` pour :
|
||||
|
||||
- limites configurables si justifiées ;
|
||||
- deadlines ;
|
||||
- backpressure/erreurs observables ;
|
||||
- close/reset/abort ;
|
||||
- cancellation/drop ;
|
||||
- cas négatifs de framing ;
|
||||
- mapping d'erreurs détaillé ;
|
||||
- tests de robustesse ciblés.
|
||||
|
||||
Un défaut fermé de cette tranche produit d'abord `0.3.5-alpha.3.fix.N` au lieu d'ouvrir `alpha.4`.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Plan 0.3.5 — POC WebTransport/QUIC et fallback WebSocket
|
||||
|
||||
## Statut
|
||||
|
||||
Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2` et son correctif de validation `0.3.5-alpha.2.fix.1`.
|
||||
Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`, son correctif de validation `0.3.5-alpha.2.fix.1` et le chemin fiable candidat de `0.3.5-alpha.3`.
|
||||
|
||||
Le cadrage détaillé et la comparaison des stacks actuelles sont conservés dans `docs/studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`. Le présent document porte les décisions opérationnelles, le scope, les gates et le forecast vivant de la version.
|
||||
|
||||
@@ -75,6 +75,23 @@ L'identité locale générée est ECDSA P-256/SHA-256, contient les SAN `localho
|
||||
|
||||
La gate de `alpha.2` a confirmé compilation, Clippy, configuration TLS/pinning et rejet d’un mauvais pin, mais le test positif comparait strictement `127.0.0.1:port` à sa représentation IPv4-mapped IPv6 `[::ffff:127.0.0.1]:port` remontée par Quinn. `alpha.2.fix.1` normalise uniquement cette représentation dans le test d’intégration ; aucun contrat, comportement transport ou scope de `alpha.3` n’est modifié.
|
||||
|
||||
### Fermeture du chemin fiable en alpha.3
|
||||
|
||||
`alpha.3` conserve `game-realtime-transport-lib` inchangé et adapte le backend concret à ses traits. Une `WebTransportSession` devient une `WebTransportConnection` après sélection d'un unique stream bidirectionnel primaire : le client l'ouvre, le serveur l'accepte. Le `split()` conserve une copie de la session dans chaque moitié afin que la session QUIC/WebTransport ne soit pas fermée au moment où l'objet connexion est consommé.
|
||||
|
||||
Le framing privé est exactement :
|
||||
|
||||
```text
|
||||
u32 big-endian payload length
|
||||
payload bytes
|
||||
```
|
||||
|
||||
La borne POC est fixée à 1 MiB par message dans cette tranche. Elle est vérifiée avant écriture et, surtout, avant allocation côté réception. Cette limite reste interne : `alpha.4` décide sa configuration produit avec deadlines, backpressure, reset/abort/cancellation et mapping d'erreurs détaillé.
|
||||
|
||||
Le FIN du stream primaire constitue la fermeture logique de base de `alpha.3` et produit `TransportReceive::Closed` après consommation des messages déjà écrits. La fermeture de session complète et les scénarios de lifecycle avancés restent explicitement réservés à `alpha.4`.
|
||||
|
||||
`web-transport-quinn` écrit l'en-tête WebTransport du stream durant `open_bi()` avant de rendre le stream au code appelant. L'accept serveur peut donc terminer avant la première frame applicative ; le framing games.sasedev commence directement au premier octet applicatif et n'ajoute aucun préambule de visibilité.
|
||||
|
||||
### Ownership physique
|
||||
|
||||
Nouveau backend durable candidat :
|
||||
@@ -380,13 +397,18 @@ Pas encore d'implémentation complète `RealtimeConnection` si cela rend la tran
|
||||
|
||||
### `0.3.5-alpha.3` — stream fiable et contrat commun
|
||||
|
||||
Tranche candidate matérialisée avec :
|
||||
|
||||
- stream bidirectionnel principal ;
|
||||
- framing `u32 + payload` borné ;
|
||||
- framing `u32 + payload` borné avant allocation ;
|
||||
- borne POC interne de 1 MiB ;
|
||||
- `RealtimeConnection`, sender et receiver ;
|
||||
- round-trip ordonné multi-message ;
|
||||
- fermeture distante de base ;
|
||||
- round-trip ordonné multi-message, y compris payload vide et octets non UTF-8 ;
|
||||
- fermeture distante de base par FIN du stream primaire ;
|
||||
- tests loopback ciblés ;
|
||||
- USAGE si l'API/configuration devient non triviale.
|
||||
- `USAGE.md` pour la séquence session -> stream primaire -> contrat commun.
|
||||
|
||||
La gate utilisateur reste requise avant d'ouvrir `alpha.4`.
|
||||
|
||||
### `0.3.5-alpha.4` — robustesse et lifecycle
|
||||
|
||||
|
||||
59
history/0.3.5/alpha.2.fix.1.md
Normal file
59
history/0.3.5/alpha.2.fix.1.md
Normal file
@@ -0,0 +1,59 @@
|
||||
<!-- file: history/0.3.5/alpha.2.fix.1.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Historique 0.3.5-alpha.2.fix.1
|
||||
|
||||
## Statut
|
||||
|
||||
`0.3.5-alpha.2.fix.1` a été validée par l'utilisateur le 2026-09-21. Le correctif ferme l'unique défaut de la gate `alpha.2` : la comparaison d'adresse distante accepte désormais la représentation IPv4-mapped IPv6 remontée par Quinn sans relâcher la comparaison du port ou d'une adresse réellement différente.
|
||||
|
||||
Aucun autre fix n'est requis. La suite peut ouvrir directement `0.3.5-alpha.3` pour le stream bidirectionnel principal, le framing fiable borné et l'adaptation au contrat commun.
|
||||
|
||||
## Gates statiques et compilation
|
||||
|
||||
Les commandes utilisateur ont terminé proprement :
|
||||
|
||||
```text
|
||||
cargo fmt --all
|
||||
cargo fmt --all -- --check
|
||||
python3 scripts/audit_rust_workspace_rules.py
|
||||
python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history
|
||||
python3 scripts/audit_distribution_layout.py
|
||||
cargo check --workspace
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings
|
||||
```
|
||||
|
||||
La sortie confirme :
|
||||
|
||||
```text
|
||||
General Rust rule audit: clean
|
||||
Rust export completeness audit: 0 candidate(s)
|
||||
games.sasedev workspace audit: clean
|
||||
Markdown table audit: clean (5 table(s), 286 file(s))
|
||||
Distribution layout audit: clean (50 required path(s), 8 forbidden path(s) absent)
|
||||
cargo check --workspace: clean
|
||||
cargo clippy --workspace --all-targets --all-features -- -D warnings: clean
|
||||
```
|
||||
|
||||
Le workspace compilé porte `0.3.5-alpha.2.fix.1`, y compris `game-realtime-webtransport-lib` et les autres targets workspace affichées par la gate.
|
||||
|
||||
## Tests WebTransport
|
||||
|
||||
La commande :
|
||||
|
||||
```bash
|
||||
cargo test -p game-realtime-webtransport-lib --all-targets --all-features
|
||||
```
|
||||
|
||||
est entièrement propre :
|
||||
|
||||
```text
|
||||
unit tests: 4 passed; 0 failed
|
||||
establishment integration tests: 2 passed; 0 failed
|
||||
```
|
||||
|
||||
Le test positif `pinned_client_and_server_establish_a_loopback_session` passe désormais, tout comme le rejet d'un mauvais pin. La preuve d'établissement natif TLS/WebTransport de `alpha.2` est donc fermée.
|
||||
|
||||
## Conséquence
|
||||
|
||||
Le forecast reste inchangé fonctionnellement : `alpha.3` peut introduire un unique stream bidirectionnel fiable principal, le framing privé `u32` big-endian + payload borné avant allocation, `RealtimeConnection`/sender/receiver, le round-trip multi-message ordonné et la fermeture distante de base.
|
||||
Reference in New Issue
Block a user