Files
games/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md
2026-09-21 18:22:19 +02:00

25 KiB

Plan 0.3.4 — transport realtime et baseline WebSocket

Statut

Plan actif créé pendant 0.3.4-alpha.1 à partir de l'archive taggée v0.3.3.

Les tranches alpha.1, alpha.2, alpha.3.fix.1 et alpha.4 ont été validées le 2026-09-21. alpha.3 avait été rejetée avant compilation à cause d'un héritage Cargo invalide de default-features; son fix a ensuite validé le backend WebSocket, le round-trip localhost et le graphe sans TLS. alpha.4 a fermé les limites, timeouts, cas négatifs et lifecycle. La revue post-gate a toutefois identifié une preuve encore absente : un smoke runtime exécuté hors harness de test. La tranche active alpha.5 ajoute donc ce smoke avant toute entrée en beta.

Mission

0.3.4 doit introduire une frontière de transport realtime async et une première implémentation WebSocket fondée sur Tokio + tokio-tungstenite, sans implémenter le protocole de session, la synchronisation gameplay ni la simulation authoritative.

La frontière reste :

transport
    ↓
wire codec
    ↓
session protocol
    ↓
synchronization
    ↓
authoritative simulation

Le transport de 0.3.4 transporte des octets opaques. Il ne connaît ni joueur, ni room, ni tick, ni snapshot, ni delta métier.

Baseline auditée

L'archive fournie comme téléchargement du tag v0.3.3 est traitée comme autoritaire conformément à CMD-GIT-003 et CMD-GIT-004.

L'audit local de l'archive confirme :

  • archive ZIP intègre, sans chemin traversant ni symlink ;
  • 400 fichiers, 14 membres Cargo et 55 fichiers Rust ;
  • workspace.package.version = 0.3.3 avant migration ;
  • audits Rust/workspace, Markdown et distribution propres sur l'archive ;
  • aucune arborescence générée target/, node_modules/, gen/android ou build/ livrée ;
  • gameplay et moteurs sans dépendance réseau realtime ;
  • baseline Android 0.3.3 conservée et hors scope de cette version.

Le log utilisateur fourni à l'ouverture de session confirme également cargo fmt, cargo check --workspace et Clippy workspace strict sur son checkout 0.3.3. Son audit Markdown annonce davantage de fichiers que l'archive taggée fournie ; la présente version reste néanmoins construite exclusivement depuis l'archive autoritaire reçue et n'invente aucun fichier absent de celle-ci.

Migration de gouvernance

À partir de 0.3.4, les seules formes de prerelease nouvelles sont :

X.Y.Z-alpha.N
X.Y.Z-alpha.N.fix.M
X.Y.Z-beta.N
X.Y.Z-beta.N.fix.M
X.Y.Z-rc.N
X.Y.Z-rc.N.fix.M

alpha.1 remplace l'ancien rôle de cadrage 0-pre.1.

Les anciens labels 0-pre, 1-alpha, 2-beta et 3-rc restent valides uniquement comme preuves historiques déjà livrées. Les audits distinguent donc la convention courante de la compatibilité historique et n'autorisent plus l'ancien schéma pour un nouveau workspace ou un nouvel historique 0.3.4+.

Dépendances vérifiées au 2026-09-21

Versions amont retenues et introduites par alpha.3 :

Tokio                1.53.1
futures-util         0.3.34
tokio-tungstenite    0.30.0
tungstenite          0.30.0, via réexport tokio-tungstenite

Contraintes utiles :

  • Tokio 1.53.1 annonce un MSRV 1.71 ;
  • tokio-tungstenite 0.30.0 et tungstenite 0.30.0 annoncent un MSRV 1.85 ;
  • tokio-tungstenite dépend déjà de Tokio 1.x et de Tungstenite 0.30.0 ;
  • ses features par défaut couvrent connect et handshake, sans TLS ;
  • native-tls et les variantes rustls-* sont optionnelles ;
  • Tungstenite expose déjà des limites de message/frame et de write buffer configurables.

alpha.3.fix.1 centralise désormais aussi default-features = false sous [workspace.dependencies], car Cargo interdit à une dépendance membre héritée de remplacer cette option. game-realtime-websocket-lib ajoute seulement les features locales sink + std pour futures-util et connect + handshake pour tokio-tungstenite; aucune feature TLS n'est activée. La gate utilisateur du fix confirme Rust 1.94.1, Tokio 1.53.1, tokio-tungstenite/Tungstenite 0.30.0 et un graphe normal sans pile TLS. alpha.4 ajoute uniquement la feature Tokio time au backend afin d'appliquer les deadlines produit.

Ownership physique retenu

game-realtime-transport-lib

Créer sous :

crates/common/game-realtime-transport-lib

Responsabilités :

  • contrat transport-neutral ;
  • payload binaire opaque ;
  • distinction send/receive/close ;
  • fermeture distante explicite ;
  • catégories d'erreur transport-neutral ;
  • contrat de split permettant lecture et écriture concurrentes ;
  • aucune dépendance à Tokio, Tungstenite, HTTP, TLS, SDL, Tauri, WASM, moteur ou gameplay.

Cette crate est une capability technique commune indépendante d'une génération de moteur. Elle n'est pas placée dans engine-v1-*.

game-realtime-websocket-lib

Créer sous :

crates/common/game-realtime-websocket-lib

Responsabilités :

  • implémentation WebSocket du contrat transport ;
  • tokio-tungstenite et Tokio réseau/temps ;
  • connexion client WebSocket ;
  • bind/accept serveur local ;
  • mapping des frames WebSocket vers le contrat binaire ;
  • close handshake, erreurs, timeouts, limites et tracing spécifiques au backend ;
  • tests loopback localhost.

Le backend dépend de game-realtime-transport-lib. L'inverse est interdit.

Dépendances interdites

Aucune crate sous :

crates/games/
crates/engines/

doit dépendre de tokio-tungstenite ou du backend WebSocket pendant 0.3.4.

Aucun contrat gameplay n'est déplacé vers les crates transport pour fabriquer artificiellement un consommateur.

Contrat transport minimal

Le contrat public doit rester suffisamment petit pour être implémentable par WebSocket puis confronté au POC WebTransport de 0.3.5.

Payload

Le message transport est un buffer binaire possédé, conceptuellement Vec<u8>.

Décisions :

  • aucune variante texte dans l'API transport-neutral ;
  • aucun JSON, Serde, Protobuf ou codec gameplay dans 0.3.4 ;
  • Ping/Pong WebSocket reste un détail du backend ;
  • une frame Text reçue par le backend n'est pas promue en message métier et doit produire un comportement explicite, testé et tracé ;
  • le futur wire codec consommera/produira ces octets au-dessus du transport.

Connexion et split

Le contrat doit permettre conceptuellement :

connection.split() -> sender + receiver
sender.send(bytes) -> async Result
sender.close() -> async Result
receiver.receive() -> async Result<Message | Closed>

alpha.2 retient une API statiquement dispatchée fondée sur des associated types : RealtimeConnection produit un Sender et un Receiver, puis les opérations async exposent des futures associées GAT (SendFuture, CloseFuture, ReceiveFuture). Le contrat commun n'utilise ni async-trait, ni Box<dyn Future>, ni runtime concret.

Aucune borne Send n'est imposée aux futures par le contrat transport-neutral : un backend natif peut naturellement fournir des futures Send, tandis qu'un futur backend navigateur/WASM ne doit pas être rendu impossible par une contrainte de threading qui ne relève pas du transport abstrait.

Le contrat doit supporter lecture et écriture concurrentes après split. Il n'expose pas de runtime Tokio et ne crée aucun executor.

Fermeture

La fermeture distante n'est pas une erreur I/O générique. receive() doit pouvoir distinguer au minimum :

  • message binaire reçu ;
  • fermeture distante propre ;
  • erreur de transport.

La fermeture locale est explicite via close().

Une cancellation de task/future n'est pas assimilée à une fermeture WebSocket propre. Abandonner le propriétaire de la connexion doit néanmoins libérer les ressources sans tâche backend détachée.

Erreurs

Le contrat transport-neutral doit représenter au minimum les catégories suivantes sans exposer les types d'erreur de Tungstenite :

invalid endpoint/configuration
connect/bind/accept failure
timeout
message too large
backpressure/write buffer full
connection closed
I/O failure
protocol/backend failure
cancelled/operation aborted lorsque cette distinction est réellement observable

Les détails backend peuvent être conservés pour Display/source/tracing sans faire remonter tungstenite::Error dans l'API commune.

Ownership Tokio/runtime

Le runtime est possédé par l'application, le service ou le test consommateur.

game-realtime-websocket-lib :

  • utilise les primitives Tokio nécessaires ;
  • ne crée pas de runtime global ;
  • ne lance pas de thread runtime privé ;
  • évite les tâches détachées pour la connexion de base ;
  • ne propage aucune dépendance Tokio dans le gameplay.

Les tests peuvent utiliser #[tokio::test] comme harness de validation, sans transformer la macro en contrat public.

Client et serveur

La séparation retenue est asymétrique et minimale :

  • le contrat commun modélise une connexion déjà établie et ses deux moitiés ;
  • le backend WebSocket possède les constructeurs concrets client/server ;
  • aucun trait générique Connector, Listener, Server, Provider ou Runtime n'est créé dans alpha.2 sans besoin démontré ;
  • le client peut exposer une connexion à partir d'un endpoint WebSocket ;
  • le serveur peut binder une adresse socket puis accepter une connexion ;
  • le futur backend WebTransport pourra proposer ses propres étapes d'établissement tout en produisant une connexion compatible lorsque cela reste pertinent.

Cette décision évite de forcer aujourd'hui WebSocket et QUIC à partager artificiellement des opérations d'établissement différentes.

TLS

La baseline 0.3.4 est d'abord un POC transport local reproductible en ws://.

Aucune feature TLS de tokio-tungstenite n'est activée par défaut dans la première implémentation. wss:// pourra être ajouté si un consommateur direct ou la stratégie de terminaison TLS le justifie réellement.

Cette décision :

  • réduit les dépendances initiales ;
  • évite de choisir prématurément native-tls contre rustls ;
  • n'interdit pas une terminaison TLS future en reverse proxy ;
  • ne présente pas le WebSocket local de test comme configuration de production Internet.

Timeouts et backpressure

Principes

  • aucune file interne non bornée ;
  • send() attend la capacité du sink au lieu d'accumuler des messages arbitrairement ;
  • les limites Tungstenite sont configurées explicitement et ne restent pas implicitement aux maxima amont ;
  • l'absence de message reçu n'est pas un timeout de transport automatique : heartbeat/idle/session timeout appartient à la couche supérieure ;
  • connect, send et fermeture propre disposent de bornes configurables afin qu'une opération locale ne reste pas bloquée indéfiniment.

Valeurs de baseline proposées

Valeurs initiales, configurables et non assimilées à un protocole gameplay final :

max message size       1 MiB
max frame size         1 MiB
write buffer target    64 KiB
max write buffer       2 MiB
connect timeout        10 s
send timeout            5 s
close timeout           2 s
receive idle timeout    aucun au niveau transport

Le choix 2 MiB pour le write buffer maximum laisse au moins la place au buffer cible plus un message maximal. Les tests négatifs doivent vérifier le comportement limite plutôt que supposer que ces constantes resteront éternellement inchangées.

Tracing

Domaines proposés :

games::realtime::transport
games::realtime::websocket

Le tracing doit couvrir au minimum connexion, accept, fermeture, timeout, rejet de message hors limite et erreurs backend.

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 :

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-*.

Décisions matérialisées dans alpha.4

game-realtime-websocket-lib expose désormais WebSocketConfig et les variantes configurables connect_with_config() et WebSocketListener::bind_with_config(). Les wrappers historiques connect() et bind() conservent des valeurs de baseline explicites :

max message size       1 MiB
max frame size         1 MiB
write buffer target    64 KiB
max write buffer       2 MiB
connect/handshake      10 s
send                     5 s
close                    2 s
receive idle timeout    aucun

La configuration refuse une limite nulle, une frame plus grande que le message, un write buffer maximum incapable de contenir le target plus un message maximal et une deadline nulle. Les limites Tungstenite sont transmises aux handshakes client et serveur ; les tailles message/frame sortantes sont également vérifiées avant l'appel au sink afin que le rejet soit déterministe.

connect_timeout borne la connexion client et la phase de handshake serveur après accept TCP. Le listener reste volontairement capable d'attendre indéfiniment un nouveau peer : ce temps d'attente appartient au service consommateur, pas à une connexion déjà en établissement. send_timeout et close_timeout bornent leurs opérations respectives. Après expiration d'un send, la moitié émission refuse un nouvel envoi avec Aborted, car l'appel interrompu peut avoir progressé partiellement et ne doit pas être rejoué implicitement.

Aucun idle timeout n'est ajouté à receive(). Heartbeat, inactivité joueur et politique de session restent au-dessus du transport.

Les tests négatifs alpha.4 couvrent :

  • rejet sortant d'un payload hors limite avant écriture ;
  • rejet entrant d'un message/frame hors limite par Tungstenite ;
  • rejet d'une frame Text par le contrat binaire ;
  • drop TCP/WebSocket sans close handshake, attendu comme erreur de protocole ;
  • timeout de handshake serveur avec un peer TCP silencieux ;
  • mapping déterministe de WriteBufferFull vers Backpressure et de Capacity vers MessageTooLarge.

Un test réseau artificiel de saturation WriteBufferFull n'est pas retenu : Tungstenite documente que son write buffer ne dépasse le target que lorsque les écritures sous-jacentes échouent, ce qui rendrait une saturation loopback normale non représentative et potentiellement flaky. La borne finie est néanmoins configurée, le mapping backend est testé unitairement et le timeout d'émission borne l'attente du sink.

Tests retenus

Contrat commun

Tests unitaires ciblés :

  • types d'erreur et affichage ;
  • invariants de configuration réellement portés par la crate commune ;
  • aucune dépendance au backend concret.

Backend WebSocket

Tests d'intégration localhost déterministes avec 127.0.0.1:0 :

  1. bind serveur sur port éphémère ;
  2. connexion client ;
  3. client → serveur : payload binaire ;
  4. serveur → client : réponse binaire ;
  5. fermeture propre initiée d'un côté et observée de l'autre ;
  6. dépassement de taille rejeté ;
  7. comportement explicite sur frame Text ;
  8. connexion interrompue/peer drop ;
  9. timeout d'opération retenu ;
  10. backpressure/write-buffer error provoquée par un test borné si elle est reproductible sans test flaky.

Les tests ne dépendent ni d'Internet, ni d'un serveur externe, ni d'un port fixe.

Un petit launcher de smoke n'était pas prévu par défaut tant que les tests publics semblaient suffire. La revue après validation de alpha.4 a distingué correctement test automatisé et smoke runtime : alpha.5 ajoute donc game-realtime-websocket-smoke sous crates/apps/. Ce binaire reste un harness de validation, pas un produit ni un protocole applicatif.

Gates par jalon

alpha.1

Changements : gouvernance, audit Python, version workspace, plan et delta.

Gate utilisateur :

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

Aucun test runtime réseau n'est requis avant l'existence du code réseau.

alpha.2

Ajouter game-realtime-transport-lib, ses tests ciblés et sa documentation publique.

Gate minimale supplémentaire :

cargo test -p game-realtime-transport-lib --all-targets --all-features

alpha.3

Ajouter game-realtime-websocket-lib, les dépendances backend et le loopback principal.

Gates ciblées :

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

alpha.4

Fermer robustesse, limites, timeouts, close/cancellation et cas négatifs. Réexécuter les tests des deux crates. Un demo n'est ajouté que si une preuve manque réellement.

Gates ciblées :

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

Le test backend doit maintenant couvrir le loopback positif, les invariants de configuration, les mappings de capacité/backpressure et les cinq scénarios négatifs déterministes de robustness.rs.

alpha.5

Ajouter le smoke runtime public avant promotion de maturité.

Gates ciblées :

cargo test -p game-realtime-transport-lib --all-targets --all-features
cargo test -p game-realtime-websocket-lib --all-targets --all-features
cargo run -p game-realtime-websocket-smoke
cargo tree -p game-realtime-websocket-smoke --edges normal

Le smoke doit binder 127.0.0.1:0, connecter un client via l'API publique, échanger un payload binaire dans chaque sens, effectuer un close propre, afficher game-realtime-websocket-smoke: PASS et sortir avec le code zéro. Il ne dépend ni d'Internet, ni d'un port fixe, ni d'un harness #[test].

Beta

La beta est le jalon large retenu pour :

cargo test --workspace --all-targets --all-features

Elle revalide également audits, format/check, Clippy, tests loopback et graphe de dépendances.

RC

La RC gèle le comportement et rejoue les gates de publication utiles. Le test workspace complet n'est répété que si un fix depuis la beta a modifié du Rust, des dépendances ou une frontière transverse qui le justifie.

Forecast révisé

0.3.4-alpha.1 — gouvernance, audit et contrat

  • migrer les règles prospectives ;
  • adapter l'audit de version sans casser l'historique ;
  • synchroniser la version workspace ;
  • vérifier dépendances et contraintes amont ;
  • décider ownership, contrat, erreurs, lifecycle, limites et tests ;
  • créer le présent plan.

Aucune dépendance réseau n'est ajoutée.

0.3.4-alpha.2 — API transport-neutral

  • créer game-realtime-transport-lib ;
  • implémenter message, receive/close, erreurs et split ;
  • conserver l'API sans Tokio/Tungstenite ;
  • retenir des futures associées GAT afin de préserver le dispatch statique sans box ni contrainte Send imposée au contrat ;
  • tests unitaires ciblés ;
  • README/USAGE uniquement si une valeur durable est démontrée par DOC-CRATE-*.

0.3.4-alpha.3 — backend WebSocket et loopback

  • créer game-realtime-websocket-lib ;
  • ajouter Tokio, tokio-tungstenite et futures-util avec features minimales ;
  • client connect + serveur bind/accept ;
  • mapping binaire, tracing, close ;
  • round-trip localhost déterministe.

0.3.4-alpha.4 — robustesse et consolidation technique

  • WebSocketConfig avec limites et deadlines explicites ;
  • application symétrique de la configuration aux handshakes client/serveur ;
  • timeout connect/handshake, send et close, sans idle timeout transport ;
  • fermeture distante propre conservée et abrupt drop distingué ;
  • cas Text non supporté ;
  • message/frame hors limite ;
  • write buffer maximum borné et mapping Backpressure testé sans fabriquer un test réseau flaky ;
  • lifecycle sans runtime/task backend privé ;
  • documentation API/backend et audit du graphe.

La gate alpha.4 est propre, mais elle ne constitue pas à elle seule un smoke runtime : tous ses scénarios restent exécutés sous le harness de test Cargo.

0.3.4-alpha.5 — smoke runtime end-to-end

  • créer game-realtime-websocket-smoke comme launcher minimal sous crates/apps/ ;
  • utiliser uniquement les API publiques game-realtime-websocket-lib et game-realtime-transport-lib ;
  • binder un listener loopback sur port éphémère et connecter un client réel ;
  • vérifier client → serveur puis serveur → client ;
  • initier et observer un close propre ;
  • borner le smoke global et retourner un code de sortie non nul au moindre défaut ;
  • ne créer ni protocole session/gameplay, ni serveur externe, ni port fixe.

DOC-CRATE-* est réévalué pour ce launcher : le workflow se résume à une commande Cargo unique et le plan central décrit entièrement sa responsabilité, donc aucun README.md/USAGE.md local n'est ajouté.

0.3.4-beta.1 — validation large

  • aucun nouveau scope ;
  • test workspace complet planifié ;
  • tests loopback/negatifs ;
  • dépendances et frontières vérifiées ;
  • confirmation qu'aucun gameplay/engine ne dépend du backend WebSocket.

Un défaut fermé produit beta.1.fix.N. Une capacité manquante réouvre une alpha.

0.3.4-rc.1 — candidate gelée

  • scope gelé ;
  • CHANGELOG.md ;
  • ROADMAP.md si statut macro à réconcilier ;
  • historique beta ;
  • documentation finale ;
  • prompt 0.3.5 préparant WebTransport/QUIC sur la même frontière ;
  • aucune abstraction nouvelle sans défaut de release.

0.3.4 — stable

Promotion mécanique : version stable, historique RC, changelog stable, clôture du plan, delta final et ajustement mécanique du prompt suivant.

Sizing

Le scope reste compatible avec une seule version/session : deux petites crates techniques, un seul backend concret et des tests locaux. Le POC WebTransport/QUIC, la session multijoueur et la simulation authoritative sont explicitement exclus.

Un split vers une autre version est requis si l'une des conditions suivantes apparaît :

  • besoin d'un vrai wire codec partagé pour prouver le transport ;
  • nécessité de concevoir le protocole session/joueur/room ;
  • TLS direct nécessitant une politique de certificats/PKI produit ;
  • abstraction commune WebSocket/QUIC exigeant déjà des concepts spécifiques à QUIC ;
  • demo/app devenant un produit autonome plutôt qu'un harness de preuve.

Hors scope confirmé

  • Uroburas Mode 3 ;
  • matchmaking ;
  • auth complète ;
  • snapshots/deltas métier ;
  • prediction/reconciliation ;
  • rollback ;
  • persistence gameplay ;
  • Redis/NATS/Kafka ;
  • scaling/sharding/regions ;
  • WebTransport/QUIC productif ;
  • Actix Web dans le data plane realtime ;
  • modification du pipeline Android 0.3.3 sans régression causée par 0.3.4.

Critères d'entrée en beta

0.3.4 peut entrer en beta lorsque :

  • l'API commune ne dépend ni de Tokio ni de WebSocket ;
  • le backend WebSocket implémente le contrat public sans fuite de types Tungstenite ;
  • client et serveur round-tripent des payloads binaires sur localhost ;
  • close local/distant est observable ;
  • limites et timeouts retenus sont testés ;
  • aucun queueing non borné ni runtime privé n'est introduit ;
  • les targets tracing transport/WebSocket sont distinctes des futurs domaines session/sync/simulation ;
  • aucune crate gameplay ou engine ne dépend de tokio-tungstenite ;
  • les audits et tests ciblés sont propres ;
  • le smoke cargo run -p game-realtime-websocket-smoke passe réellement côté utilisateur hors harness de test ;
  • la documentation décrit ce qui est réellement implémenté et ce qui reste reporté.