0.3.4-alpha.3

This commit is contained in:
2026-09-21 17:40:56 +02:00
parent 4e2be3cc9a
commit 4fb521f54e
9 changed files with 636 additions and 9 deletions

View File

@@ -1,5 +1,5 @@
# file: Cargo.toml
# version: 87
# version: 88
[workspace]
resolver = "3"
@@ -14,6 +14,7 @@ members = [
"crates/common/game-assets-lib",
"crates/common/game-logging-lib",
"crates/common/game-realtime-transport-lib",
"crates/common/game-realtime-websocket-lib",
"crates/apps/game-android-entrypoint",
"crates/apps/game-reflex-poc-tauri",
"crates/apps/game-reflex-poc-wasm",
@@ -22,7 +23,7 @@ members = [
]
[workspace.package]
version = "0.3.4-alpha.2"
version = "0.3.4-alpha.3"
edition = "2024"
license = "MIT"
repository = "https://git.sasedev.com/Sasedev/games"
@@ -30,6 +31,7 @@ authors = ["Sasedev <games@sasedev.com>"]
publish = false
[workspace.dependencies]
futures-util = "0.3.34"
serde = { version = "1", features = ["derive"] }
sdl3 = "^0.20"
tracing = "0.1.44"
@@ -39,6 +41,8 @@ tracing-subscriber = "0.3.23"
tauri = "2"
tauri-build = "2"
tauri-plugin-tracing = "^0.3"
tokio = "1.53.1"
tokio-tungstenite = "0.30.0"
wasm-bindgen = "0.2"
[workspace.lints.rust]

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 55 -->
<!-- version: 56 -->
# games.sasedev
@@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale
Version stable de référence : `0.3.3`.
Version candidate active : `0.3.4-alpha.2`. `0.3.2` reste différée.
Version candidate active : `0.3.4-alpha.3`. `0.3.2` reste différée.
La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.2` introduit la première API transport-neutral realtime : messages binaires opaques, fermeture distante explicite, erreurs catégorisées et split statique sender/receiver, sans dépendance Tokio ou WebSocket.
La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.3` conserve l'API transport-neutral de `alpha.2` et ajoute un backend WebSocket séparé fondé sur Tokio + `tokio-tungstenite`, avec client `ws://`, listener serveur, split sender/receiver, tracing et round-trip localhost déterministe.
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.

View File

@@ -0,0 +1,24 @@
# file: crates/common/game-realtime-websocket-lib/Cargo.toml
# version: 1
[package]
name = "game-realtime-websocket-lib"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
authors.workspace = true
publish.workspace = true
[dependencies]
futures-util = { workspace = true, default-features = false, features = ["sink", "std"] }
game-realtime-transport-lib = { path = "../game-realtime-transport-lib" }
tokio = { workspace = true, features = ["net"] }
tokio-tungstenite = { workspace = true, default-features = false, features = ["connect", "handshake"] }
tracing.workspace = true
[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt", "time"] }
[lints]
workspace = true

View File

@@ -0,0 +1,21 @@
// file: crates/common/game-realtime-websocket-lib/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Tokio/tokio-tungstenite WebSocket backend for the games.sasedev realtime transport contract.
mod websocket;
/// Re-export of the WebSocket client connection constructor.
pub use self::websocket::connect;
/// Re-export of an established WebSocket transport connection.
pub use self::websocket::WebSocketConnection;
/// Re-export of the receive half of an established WebSocket connection.
pub use self::websocket::WebSocketReceiver;
/// Re-export of a bound WebSocket server listener.
pub use self::websocket::WebSocketListener;
/// Re-export of the send half of an established WebSocket connection.
pub use self::websocket::WebSocketSender;

View File

@@ -0,0 +1,300 @@
// file: crates/common/game-realtime-websocket-lib/src/websocket.rs
// version: 1
use futures_util::SinkExt; // rust-rules: trait-import
use futures_util::StreamExt; // rust-rules: trait-import
const TRACING_TARGET: &str = "games::realtime::websocket";
type ClientStream = tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
type ServerStream = tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>;
type ClientSink = futures_util::stream::SplitSink<ClientStream, tokio_tungstenite::tungstenite::Message>;
type ServerSink = futures_util::stream::SplitSink<ServerStream, tokio_tungstenite::tungstenite::Message>;
type ClientReceiver = futures_util::stream::SplitStream<ClientStream>;
type ServerReceiver = futures_util::stream::SplitStream<ServerStream>;
enum WebSocketStreamKind {
Client(ClientStream),
Server(ServerStream),
}
enum WebSocketSinkKind {
Client(ClientSink),
Server(ServerSink),
}
enum WebSocketReceiverKind {
Client(ClientReceiver),
Server(ServerReceiver),
}
/// Established WebSocket connection implementing the transport-neutral realtime contract.
pub struct WebSocketConnection {
inner: WebSocketStreamKind,
}
impl WebSocketConnection {
fn from_client(stream: ClientStream) -> Self {
return Self { inner: WebSocketStreamKind::Client(stream) };
}
fn from_server(stream: ServerStream) -> Self {
return Self { inner: WebSocketStreamKind::Server(stream) };
}
}
impl game_realtime_transport_lib::RealtimeConnection for WebSocketConnection {
type Sender = crate::WebSocketSender;
type Receiver = crate::WebSocketReceiver;
fn split(self) -> (Self::Sender, Self::Receiver) {
return match self.inner {
WebSocketStreamKind::Client(stream) => {
let (sender, receiver) = stream.split();
(
crate::WebSocketSender { inner: WebSocketSinkKind::Client(sender) },
crate::WebSocketReceiver { inner: WebSocketReceiverKind::Client(receiver) },
)
},
WebSocketStreamKind::Server(stream) => {
let (sender, receiver) = stream.split();
(
crate::WebSocketSender { inner: WebSocketSinkKind::Server(sender) },
crate::WebSocketReceiver { inner: WebSocketReceiverKind::Server(receiver) },
)
},
};
}
}
/// Send half of an established WebSocket transport connection.
pub struct WebSocketSender {
inner: WebSocketSinkKind,
}
impl game_realtime_transport_lib::RealtimeSender for WebSocketSender {
type SendFuture<'a> = futures_util::future::LocalBoxFuture<'a, Result<(), game_realtime_transport_lib::TransportError>> where Self: 'a;
type CloseFuture<'a> = futures_util::future::LocalBoxFuture<'a, Result<(), game_realtime_transport_lib::TransportError>> where Self: 'a;
fn send(&mut self, message: game_realtime_transport_lib::TransportMessage) -> Self::SendFuture<'_> {
return Box::pin(async move {
let payload_len = message.len();
let websocket_message = tokio_tungstenite::tungstenite::Message::Binary(message.into_bytes().into());
let result = match &mut self.inner {
WebSocketSinkKind::Client(sender) => sender.send(websocket_message).await,
WebSocketSinkKind::Server(sender) => sender.send(websocket_message).await,
};
return match result {
Ok(()) => {
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "binary WebSocket payload sent");
Ok(())
},
Err(error) => {
let mapped = map_stream_error(error);
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "WebSocket send failed");
Err(mapped)
},
};
});
}
fn close(&mut self) -> Self::CloseFuture<'_> {
return Box::pin(async move {
let result = match &mut self.inner {
WebSocketSinkKind::Client(sender) => sender.close().await,
WebSocketSinkKind::Server(sender) => sender.close().await,
};
return match result {
Ok(()) => {
tracing::debug!(target: TRACING_TARGET, "local WebSocket close initiated");
Ok(())
},
Err(error) => {
let mapped = map_stream_error(error);
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "WebSocket close failed");
Err(mapped)
},
};
});
}
}
/// Receive half of an established WebSocket transport connection.
pub struct WebSocketReceiver {
inner: WebSocketReceiverKind,
}
impl game_realtime_transport_lib::RealtimeReceiver for WebSocketReceiver {
type ReceiveFuture<'a> =
futures_util::future::LocalBoxFuture<'a, Result<game_realtime_transport_lib::TransportReceive, game_realtime_transport_lib::TransportError>>
where
Self: 'a;
fn receive(&mut self) -> Self::ReceiveFuture<'_> {
return Box::pin(async move {
loop {
let next_message = match &mut self.inner {
WebSocketReceiverKind::Client(receiver) => receiver.next().await,
WebSocketReceiverKind::Server(receiver) => receiver.next().await,
};
match next_message {
Some(Ok(tokio_tungstenite::tungstenite::Message::Binary(bytes))) => {
tracing::trace!(target: TRACING_TARGET, payload_len = bytes.len(), "binary WebSocket payload received");
return Ok(game_realtime_transport_lib::TransportReceive::Message(
game_realtime_transport_lib::TransportMessage::new(bytes.to_vec()),
));
},
Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => {
tracing::debug!(target: TRACING_TARGET, "remote WebSocket close observed");
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
},
Some(Ok(tokio_tungstenite::tungstenite::Message::Ping(_)))
| Some(Ok(tokio_tungstenite::tungstenite::Message::Pong(_))) => {},
Some(Ok(tokio_tungstenite::tungstenite::Message::Text(_))) => {
let error = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Protocol,
"text WebSocket messages are not part of the binary transport contract",
);
tracing::warn!(target: TRACING_TARGET, kind = %error.kind(), "unsupported WebSocket text message received");
return Err(error);
},
Some(Ok(tokio_tungstenite::tungstenite::Message::Frame(_))) => {
let error = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Protocol,
"unexpected raw WebSocket frame surfaced by the backend",
);
tracing::warn!(target: TRACING_TARGET, kind = %error.kind(), "unexpected raw WebSocket frame received");
return Err(error);
},
Some(Err(error)) => {
let mapped = map_stream_error(error);
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "WebSocket receive failed");
return Err(mapped);
},
None => {
tracing::debug!(target: TRACING_TARGET, "WebSocket stream ended");
return Ok(game_realtime_transport_lib::TransportReceive::Closed);
},
}
}
});
}
}
/// Bound TCP listener that upgrades accepted peers to WebSocket connections.
pub struct WebSocketListener {
listener: tokio::net::TcpListener,
local_addr: std::net::SocketAddr,
}
impl WebSocketListener {
/// Binds a WebSocket listener to one concrete socket address.
pub async fn bind(address: std::net::SocketAddr) -> Result<Self, game_realtime_transport_lib::TransportError> {
let listener = match tokio::net::TcpListener::bind(address).await {
Ok(value) => value,
Err(error) => {
let mapped = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Bind,
error.to_string(),
);
tracing::warn!(target: TRACING_TARGET, address = %address, detail = mapped.detail(), "WebSocket listener bind failed");
return Err(mapped);
},
};
let local_addr = match listener.local_addr() {
Ok(value) => value,
Err(error) => {
let mapped = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Bind,
error.to_string(),
);
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "bound WebSocket listener address lookup failed");
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, address = %local_addr, "WebSocket listener bound");
return Ok(Self { listener, local_addr });
}
/// Returns the concrete local socket address, including an ephemeral port selected by the OS.
#[must_use]
pub fn local_addr(&self) -> std::net::SocketAddr {
return self.local_addr;
}
/// Accepts one TCP peer and completes the server-side WebSocket handshake.
pub async fn accept(&self) -> Result<crate::WebSocketConnection, game_realtime_transport_lib::TransportError> {
let (stream, peer_addr) = match self.listener.accept().await {
Ok(value) => value,
Err(error) => {
let mapped = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Accept,
error.to_string(),
);
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "WebSocket TCP accept failed");
return Err(mapped);
},
};
let websocket = match tokio_tungstenite::accept_async(stream).await {
Ok(value) => value,
Err(error) => {
let mapped = game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::Accept,
error.to_string(),
);
tracing::warn!(target: TRACING_TARGET, peer = %peer_addr, detail = mapped.detail(), "WebSocket server handshake failed");
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, peer = %peer_addr, "WebSocket peer accepted");
return Ok(crate::WebSocketConnection::from_server(websocket));
}
}
/// Connects a client to one plain `ws://` endpoint and completes the WebSocket handshake.
pub async fn connect(endpoint: &str) -> Result<crate::WebSocketConnection, game_realtime_transport_lib::TransportError> {
if !endpoint.starts_with("ws://") {
return Err(game_realtime_transport_lib::TransportError::new(
game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration,
"the baseline WebSocket backend accepts only ws:// endpoints",
));
}
tracing::debug!(target: TRACING_TARGET, endpoint = endpoint, "connecting WebSocket client");
let (stream, _) = match tokio_tungstenite::connect_async(endpoint).await {
Ok(value) => value,
Err(error) => {
let mapped = map_connect_error(error);
tracing::warn!(
target: TRACING_TARGET,
endpoint = endpoint,
kind = %mapped.kind(),
detail = mapped.detail(),
"WebSocket client connect failed"
);
return Err(mapped);
},
};
tracing::info!(target: TRACING_TARGET, endpoint = endpoint, "WebSocket client connected");
return Ok(crate::WebSocketConnection::from_client(stream));
}
fn map_connect_error(error: tokio_tungstenite::tungstenite::Error) -> game_realtime_transport_lib::TransportError {
let kind = match &error {
tokio_tungstenite::tungstenite::Error::Url(_) => game_realtime_transport_lib::TransportErrorKind::InvalidConfiguration,
_ => game_realtime_transport_lib::TransportErrorKind::Connect,
};
return game_realtime_transport_lib::TransportError::new(kind, error.to_string());
}
fn map_stream_error(error: tokio_tungstenite::tungstenite::Error) -> game_realtime_transport_lib::TransportError {
let kind = match &error {
tokio_tungstenite::tungstenite::Error::ConnectionClosed | tokio_tungstenite::tungstenite::Error::AlreadyClosed => {
game_realtime_transport_lib::TransportErrorKind::Closed
},
tokio_tungstenite::tungstenite::Error::Io(_) => game_realtime_transport_lib::TransportErrorKind::Io,
tokio_tungstenite::tungstenite::Error::Capacity(_) => game_realtime_transport_lib::TransportErrorKind::MessageTooLarge,
tokio_tungstenite::tungstenite::Error::WriteBufferFull(_) => game_realtime_transport_lib::TransportErrorKind::Backpressure,
_ => game_realtime_transport_lib::TransportErrorKind::Protocol,
};
return game_realtime_transport_lib::TransportError::new(kind, error.to_string());
}

View File

@@ -0,0 +1,63 @@
// file: crates/common/game-realtime-websocket-lib/tests/loopback.rs
// version: 1
//! Deterministic localhost proof for the Tokio/tokio-tungstenite backend.
use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import
use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import
use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
#[tokio::test(flavor = "current_thread")]
async fn binary_round_trip_and_clean_close_work_on_loopback() {
let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0));
let listener = match game_realtime_websocket_lib::WebSocketListener::bind(bind_address).await {
Ok(value) => value,
Err(error) => panic!("loopback listener bind failed: {error}"),
};
let endpoint = format!("ws://{}/", listener.local_addr());
let pair = tokio::time::timeout(TEST_TIMEOUT, async {
return tokio::join!(listener.accept(), game_realtime_websocket_lib::connect(endpoint.as_str()));
})
.await;
let (server_connection, client_connection) = match pair {
Ok((Ok(server), Ok(client))) => (server, client),
Ok((_server, _client)) => panic!("loopback connection establishment failed"),
Err(_) => panic!("loopback connection establishment timed out"),
};
let (mut server_sender, mut server_receiver) = server_connection.split();
let (mut client_sender, mut client_receiver) = client_connection.split();
let client_payload = game_realtime_transport_lib::TransportMessage::new(vec![0, 1, 2, 3, 255]);
let client_send = tokio::time::timeout(TEST_TIMEOUT, client_sender.send(client_payload)).await;
assert!(matches!(client_send, Ok(Ok(()))));
let server_receive = tokio::time::timeout(TEST_TIMEOUT, server_receiver.receive()).await;
match server_receive {
Ok(Ok(received)) => assert_eq!(
received,
game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(vec![0, 1, 2, 3, 255]))
),
Ok(Err(error)) => panic!("server receive failed: {error}"),
Err(_) => panic!("server receive timed out"),
}
let server_payload = game_realtime_transport_lib::TransportMessage::new(vec![9, 8, 7, 6]);
let server_send = tokio::time::timeout(TEST_TIMEOUT, server_sender.send(server_payload)).await;
assert!(matches!(server_send, Ok(Ok(()))));
let client_receive = tokio::time::timeout(TEST_TIMEOUT, client_receiver.receive()).await;
match client_receive {
Ok(Ok(received)) => assert_eq!(
received,
game_realtime_transport_lib::TransportReceive::Message(game_realtime_transport_lib::TransportMessage::new(vec![9, 8, 7, 6]))
),
Ok(Err(error)) => panic!("client receive failed: {error}"),
Err(_) => panic!("client receive timed out"),
}
let client_close = tokio::time::timeout(TEST_TIMEOUT, client_sender.close()).await;
assert!(matches!(client_close, Ok(Ok(()))));
let server_close_observation = tokio::time::timeout(TEST_TIMEOUT, server_receiver.receive()).await;
match server_close_observation {
Ok(Ok(received)) => assert_eq!(received, game_realtime_transport_lib::TransportReceive::Closed),
Ok(Err(error)) => panic!("server close observation failed: {error}"),
Err(_) => panic!("server close observation timed out"),
};
}

144
deltas/0.3.4/alpha.3.md Normal file
View File

@@ -0,0 +1,144 @@
<!-- file: deltas/0.3.4/alpha.3.md -->
<!-- version: 1 -->
# Delta 0.3.4-alpha.3
## Mission
Introduire le premier backend concret de la frontière realtime validée en `alpha.2` : WebSocket async natif avec Tokio + `tokio-tungstenite`, sans TLS, sans protocole session/gameplay et sans dépendance backend dans les moteurs ou jeux.
## Historique fermé
`history/0.3.4/alpha.2.md` enregistre la validation utilisateur : audits propres, workspace/Clippy strict propres et `7` tests sur `game-realtime-transport-lib` passés.
Aucun `alpha.2.fix.N` n'est requis.
## Version et dépendances
Le workspace passe à :
```text
0.3.4-alpha.3
```
Dépendances tierces centralisées au workspace :
```text
futures-util 0.3.34
tokio 1.53.1
tokio-tungstenite 0.30.0
```
`game-realtime-websocket-lib` active uniquement les features dont elle a besoin :
```text
futures-util: default-features=false, sink, std
tokio: net
tokio-tungstenite: default-features=false, connect, handshake
```
Le harness de test ajoute localement les features Tokio `macros`, `rt` et `time`. Aucune feature TLS n'est activée.
## Nouvelle crate WebSocket
Ajout :
```text
crates/common/game-realtime-websocket-lib/Cargo.toml
crates/common/game-realtime-websocket-lib/src/lib.rs
crates/common/game-realtime-websocket-lib/src/websocket.rs
crates/common/game-realtime-websocket-lib/tests/loopback.rs
```
La crate implémente le contrat `game-realtime-transport-lib` sans en modifier l'API.
### Client
`connect(&str)` établit une connexion WebSocket cliente à partir d'un endpoint `ws://`. Un endpoint hors baseline, notamment `wss://`, est rejeté comme `InvalidConfiguration` au lieu d'activer implicitement un backend TLS.
### Serveur
`WebSocketListener::bind(SocketAddr)` bind un `tokio::net::TcpListener`; `127.0.0.1:0` permet au système de choisir un port de test éphémère. `accept()` accepte le TCP puis effectue le handshake WebSocket serveur.
### Connexion
Client et serveur retournent le même `WebSocketConnection` public. Les représentations `tokio_tungstenite::WebSocketStream<...>` restent privées.
`RealtimeConnection::split()` produit `WebSocketSender` et `WebSocketReceiver`. Les futures concrètes sont boxées uniquement dans ce backend afin d'implémenter les associated futures GAT du contrat. Les représentations WebSocket/Tungstenite restent privées ; l'alias public `LocalBoxFuture` de `futures-util` sert uniquement de représentation concrète des associated futures du backend.
### Mapping WebSocket
```text
Binary -> TransportReceive::Message
Close -> TransportReceive::Closed
Ping / Pong -> détail de contrôle backend, non remonté au gameplay
Text -> TransportErrorKind::Protocol
Frame -> TransportErrorKind::Protocol
WriteBufferFull -> TransportErrorKind::Backpressure
Capacity -> TransportErrorKind::MessageTooLarge
I/O -> TransportErrorKind::Io
closed/already -> TransportErrorKind::Closed
```
Les erreurs de connect/bind/accept restent catégorisées au niveau de l'opération qui échoue et aucun `tungstenite::Error` ne fuit dans l'API publique.
## Tracing
Le target backend est :
```text
games::realtime::websocket
```
Les événements couvrent connect, bind, accept, send, receive, close et erreurs. Le contenu brut des payloads n'est jamais loggué ; seule leur longueur peut apparaître au niveau trace.
La crate ne configure aucun subscriber global.
## Preuve loopback
Le test d'intégration public :
```text
binary_round_trip_and_clean_close_work_on_loopback
```
utilise `127.0.0.1:0`, établit client et serveur sans Internet ni port fixe, puis vérifie :
- payload binaire client vers serveur ;
- payload binaire serveur vers client ;
- fermeture locale cliente observée comme fermeture distante propre côté serveur.
Une borne de trois secondes entoure les étapes asynchrones uniquement pour rendre le test déterministe en cas de régression ; elle ne constitue pas encore le timeout de transport produit, réservé à `alpha.4`.
## Documentation locale
Aucun `README.md` ou `USAGE.md` local n'est ajouté pour cette nouvelle crate pendant `alpha.3`. Le contrat reste petit, la rustdoc décrit l'API et le plan central porte les décisions d'architecture. Cette décision sera réévaluée pendant la consolidation finale conformément à `DOC-CRATE-*`.
## Fichiers existants modifiés
```text
Cargo.toml
README.md
docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md
```
`ROADMAP.md` et `CHANGELOG.md` restent inchangés : le scope macro de `0.3.4` n'a pas changé et une alpha n'ajoute pas encore de changelog de release.
## Validation attendue
```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-websocket-lib --all-targets --all-features
cargo tree -p game-realtime-websocket-lib --edges normal
```
`cargo test --workspace --all-targets --all-features` reste réservé à la beta conformément au plan.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# Plan 0.3.4 — transport realtime et baseline WebSocket
@@ -7,7 +7,7 @@
Plan actif créé pendant `0.3.4-alpha.1` à partir de l'archive taggée `v0.3.3`.
La tranche `alpha.1` a été validée le 2026-09-21. La tranche active `alpha.2` matérialise maintenant le contrat transport-neutral sans dépendance runtime/backend ; le backend WebSocket reste réservé à `alpha.3`.
Les tranches `alpha.1` et `alpha.2` ont été validées le 2026-09-21. La tranche active `alpha.3` ajoute maintenant le backend WebSocket Tokio/tokio-tungstenite derrière le contrat transport-neutral validé ; les limites, timeouts et cas négatifs complets restent réservés à `alpha.4`.
## Mission
@@ -64,7 +64,7 @@ Les anciens labels `0-pre`, `1-alpha`, `2-beta` et `3-rc` restent valides unique
## Dépendances vérifiées au 2026-09-21
Versions amont observées pour la future implémentation :
Versions amont retenues et introduites par `alpha.3` :
```text
Tokio 1.53.1
@@ -82,7 +82,7 @@ Contraintes utiles :
- `native-tls` et les variantes `rustls-*` sont optionnelles ;
- Tungstenite expose déjà des limites de message/frame et de write buffer configurables.
La compatibilité effective de la toolchain utilisateur avec ces MSRV sera attestée par les gates Cargo de la tranche qui introduira réellement les dépendances. Elles ne sont pas ajoutées pendant `alpha.1` : elles seront introduites uniquement avec les crates qui les consomment réellement.
`alpha.3` centralise désormais ces versions sous `[workspace.dependencies]` et active les features uniquement dans `game-realtime-websocket-lib`. `futures-util` est consommé avec `default-features = false` et seulement `sink + std`; `tokio-tungstenite` est consommé avec `default-features = false` et seulement `connect + handshake`; aucune feature TLS n'est activée. La compatibilité effective de la toolchain utilisateur avec ces MSRV sera attestée par la gate Cargo utilisateur de cette tranche.
## Ownership physique retenu
@@ -284,6 +284,29 @@ Ne pas logguer par défaut le contenu brut des payloads de transport.
Les futurs domaines session/sync/simulation utilisent des targets distinctes.
## Décisions matérialisées dans `alpha.3`
Le backend concret reste sous `crates/common/game-realtime-websocket-lib` et dépend uniquement du contrat commun plus des briques réseau/tracing nécessaires.
L'API publique expose :
```text
connect(endpoint)
WebSocketListener::bind(address)
WebSocketListener::accept()
WebSocketConnection
WebSocketSender
WebSocketReceiver
```
Les types `WebSocketStream`, `SplitSink`, `SplitStream`, `MaybeTlsStream` et `tungstenite::Error` restent privés. Client et serveur produisent le même `WebSocketConnection` public grâce à une représentation interne à deux variantes ; le contrat commun reste donc la seule frontière partagée par les consommateurs.
`connect()` accepte volontairement uniquement `ws://` dans cette baseline. Le TLS direct reste différé. Les frames binaires deviennent `TransportMessage`; Close devient `TransportReceive::Closed`; Ping/Pong sont traités comme contrôle backend ; Text et raw Frame sont explicitement rejetés comme erreurs de protocole. Les tests négatifs exhaustifs correspondants restent planifiés pour `alpha.4`.
Le backend ne crée aucun runtime, thread ni task détachée. Le harness d'intégration utilise un runtime Tokio de test courant, un bind `127.0.0.1:0` et une borne temporelle uniquement pour empêcher un test loopback défectueux de rester suspendu.
Aucun `README.md`/`USAGE.md` local n'est ajouté pendant `alpha.3` : la crate reste petite, son API publique est documentée par rustdoc et le présent plan porte encore les décisions durables. Ce choix sera réévalué pendant la consolidation finale conformément à `DOC-CRATE-*`.
## Tests retenus
### Contrat commun

48
history/0.3.4/alpha.2.md Normal file
View File

@@ -0,0 +1,48 @@
<!-- file: history/0.3.4/alpha.2.md -->
<!-- version: 1 -->
# Historique 0.3.4-alpha.2
## Statut
`0.3.4-alpha.2` a été validée par l'utilisateur le 2026-09-21. Le contrat transport-neutral compile dans le workspace, passe Clippy strict et ses sept tests ciblés sont propres.
Aucun `alpha.2.fix.N` n'est requis. La suite peut ouvrir `0.3.4-alpha.3` et introduire le backend WebSocket Tokio/tokio-tungstenite planifié.
## Gates statiques et Rust
La gate fournie est propre :
```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), 262 file(s))
Distribution layout audit: clean (49 required path(s), 8 forbidden path(s) absent)
cargo fmt --all: clean
cargo fmt --all -- --check: clean
cargo check --workspace: clean
cargo clippy --workspace --all-targets --all-features -- -D warnings: clean
```
Le workspace compilé porte `0.3.4-alpha.2`, y compris `game-realtime-transport-lib`.
## Tests du contrat transport
La commande :
```text
cargo test -p game-realtime-transport-lib --all-targets --all-features
```
termine avec :
```text
7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```
Les tests couvrent le payload binaire possédé, le payload vide, la distinction message/fermeture propre, le split sender/receiver, les futures associées de send/receive/close et les catégories d'erreur transport-neutral.
## Conséquence
La frontière commune est acceptée sans Tokio, Tungstenite ni runtime concret. `alpha.3` peut ajouter une crate backend séparée qui implémente ce contrat, tout en conservant le gameplay et les moteurs indépendants du backend WebSocket.