0.3.5-alpha.7
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: Cargo.toml
|
||||
# version: 102
|
||||
# version: 103
|
||||
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
@@ -10,6 +10,7 @@ members = [
|
||||
"crates/games/game-reflex-poc",
|
||||
"crates/games/game-snake-poc",
|
||||
"crates/apps/game-realtime-websocket-smoke",
|
||||
"crates/apps/game-realtime-webtransport-browser-smoke",
|
||||
"crates/apps/game-realtime-webtransport-smoke",
|
||||
"crates/apps/game-reflex-poc-desktop",
|
||||
"crates/apps/game-snake-poc-desktop",
|
||||
@@ -26,7 +27,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.3.5-alpha.6"
|
||||
version = "0.3.5-alpha.7"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://git.sasedev.com/Sasedev/games"
|
||||
@@ -35,6 +36,7 @@ publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
futures-util = { version = "0.3.34", default-features = false }
|
||||
gloo-timers = "0.4.0"
|
||||
rcgen = { version = "0.14.10", default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
sdl3 = "^0.20"
|
||||
@@ -49,6 +51,7 @@ tokio = "1.53.1"
|
||||
tokio-tungstenite = { version = "0.30.0", default-features = false }
|
||||
url = "2.5.8"
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
web-transport-quinn = { version = "0.12.1", default-features = false }
|
||||
web-transport-wasm = "0.6.0"
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 70 -->
|
||||
<!-- version: 71 -->
|
||||
|
||||
# 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.6`. `0.3.2` reste différée.
|
||||
Version active : `0.3.5-alpha.7`. `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.6` conserve le backend WebTransport natif et son smoke `PASS`, puis ajoute un client WebTransport `wasm32-unknown-unknown` compilable fondé sur l'API navigateur : pin SHA-256 explicite, session, stream bidirectionnel primaire et framing fiable adapté au même contrat realtime. Le smoke navigateur réel, le fallback et les datagrams restent réservés aux tranches suivantes.
|
||||
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.7` conserve le backend WebTransport natif et son smoke `PASS`, valide le client `wasm32-unknown-unknown`, applique aussi les deadlines côté navigateur et ajoute un smoke browser technique Vite/TypeScript + adapter Rust/WASM vers un serveur Rust local avec pin SHA-256 exact. Le fallback et les 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 le chemin natif couvre TLS/pinning, établissement de session et stream fiable principal, tandis que son chemin client WASM compile contre l'API WebTransport du navigateur avec le même framing fiable et le même contrat commun. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` et `game-realtime-webtransport-smoke` fournissent les preuves runtime localhost hors harness des deux backends fiables natifs. 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 TLS/pinning, établissement de session et stream fiable principal, tandis que son chemin client WASM compile contre l'API WebTransport du navigateur avec le même framing fiable et le même contrat commun. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` et `game-realtime-webtransport-smoke` fournissent les preuves runtime localhost hors harness des deux backends fiables natifs ; `game-realtime-webtransport-browser-smoke` et son host sous `Web/` portent la preuve navigateur WebTransport réelle sans couplage gameplay. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`.
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>games.sasedev WebTransport browser smoke</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>WebTransport browser smoke</h1>
|
||||
<p id="environment-status">Checking browser environment…</p>
|
||||
<label>
|
||||
WebTransport endpoint
|
||||
<input id="endpoint" type="text" size="72" autocomplete="off">
|
||||
</label>
|
||||
<br>
|
||||
<label>
|
||||
Certificate SHA-256
|
||||
<input id="certificate-hash" type="text" size="72" autocomplete="off">
|
||||
</label>
|
||||
<br>
|
||||
<button id="run" type="button">Run smoke</button>
|
||||
<pre id="result">IDLE</pre>
|
||||
</main>
|
||||
<script type="module" src="./ts/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
// file: Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts
|
||||
// version: 1
|
||||
|
||||
import init, { run_browser_smoke } from "@webtransport-browser-smoke-wasm";
|
||||
|
||||
function requireElement<T extends Element>(selector: string): T {
|
||||
const element = document.querySelector<T>(selector);
|
||||
if (element === null) {
|
||||
throw new Error(`Required browser smoke element is missing: ${selector}`);
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
function populateFromQuery(endpoint: HTMLInputElement, certificateHash: HTMLInputElement): void {
|
||||
const parameters = new URLSearchParams(window.location.search);
|
||||
const endpointValue = parameters.get("endpoint");
|
||||
const hashValue = parameters.get("sha256");
|
||||
if (endpointValue !== null) {
|
||||
endpoint.value = endpointValue;
|
||||
}
|
||||
if (hashValue !== null) {
|
||||
certificateHash.value = hashValue;
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const environmentStatus = requireElement<HTMLParagraphElement>("#environment-status");
|
||||
const endpoint = requireElement<HTMLInputElement>("#endpoint");
|
||||
const certificateHash = requireElement<HTMLInputElement>("#certificate-hash");
|
||||
const run = requireElement<HTMLButtonElement>("#run");
|
||||
const result = requireElement<HTMLPreElement>("#result");
|
||||
populateFromQuery(endpoint, certificateHash);
|
||||
if (!window.isSecureContext) {
|
||||
environmentStatus.textContent = "FAIL: page is not running in a secure context";
|
||||
run.disabled = true;
|
||||
return;
|
||||
}
|
||||
if (!("WebTransport" in window)) {
|
||||
environmentStatus.textContent = "FAIL: this browser does not expose WebTransport";
|
||||
run.disabled = true;
|
||||
return;
|
||||
}
|
||||
await init();
|
||||
environmentStatus.textContent = "Secure context and WebTransport available; WASM loaded";
|
||||
run.addEventListener("click", () => {
|
||||
run.disabled = true;
|
||||
result.textContent = "RUNNING";
|
||||
void run_browser_smoke(endpoint.value.trim(), certificateHash.value.trim())
|
||||
.then(message => {
|
||||
result.textContent = message;
|
||||
})
|
||||
.catch(caughtError => {
|
||||
result.textContent = `game-realtime-webtransport-browser-smoke: FAIL: ${String(caughtError)}`;
|
||||
})
|
||||
.finally(() => {
|
||||
run.disabled = false;
|
||||
});
|
||||
});
|
||||
if (endpoint.value.length > 0 && certificateHash.value.length > 0) {
|
||||
run.click();
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch(caughtError => {
|
||||
const result = document.querySelector<HTMLPreElement>("#result");
|
||||
if (result !== null) {
|
||||
result.textContent = `game-realtime-webtransport-browser-smoke: FAIL: ${String(caughtError)}`;
|
||||
}
|
||||
});
|
||||
17
Web/game-realtime-webtransport-browser-smoke/package.json
Normal file
17
Web/game-realtime-webtransport-browser-smoke/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "game-realtime-webtransport-browser-smoke-web",
|
||||
"private": true,
|
||||
"version": "0.3.5-alpha.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"wasm:dev": "cargo build -p game-realtime-webtransport-browser-smoke --lib --target wasm32-unknown-unknown && wasm-bindgen ../../builds/sasedev-games/target/wasm32-unknown-unknown/debug/game_realtime_webtransport_browser_smoke.wasm --target web --out-dir ../../builds/sasedev-games/game-realtime-webtransport-browser-smoke/wasm --out-name game_realtime_webtransport_browser_smoke",
|
||||
"wasm:build": "cargo build -p game-realtime-webtransport-browser-smoke --lib --release --target wasm32-unknown-unknown && wasm-bindgen ../../builds/sasedev-games/target/wasm32-unknown-unknown/release/game_realtime_webtransport_browser_smoke.wasm --target web --out-dir ../../builds/sasedev-games/game-realtime-webtransport-browser-smoke/wasm --out-name game_realtime_webtransport_browser_smoke",
|
||||
"dev": "npm run wasm:dev && vite",
|
||||
"build": "npm run wasm:build && tsc && vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^26.1",
|
||||
"typescript": "^7.0",
|
||||
"vite": "^8.2"
|
||||
}
|
||||
}
|
||||
35
Web/game-realtime-webtransport-browser-smoke/tsconfig.json
Normal file
35
Web/game-realtime-webtransport-browser-smoke/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"vite/client",
|
||||
"node"
|
||||
],
|
||||
"paths": {
|
||||
"@webtransport-browser-smoke-wasm": [
|
||||
"../../builds/sasedev-games/game-realtime-webtransport-browser-smoke/wasm/game_realtime_webtransport_browser_smoke.d.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"frontend",
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
50
Web/game-realtime-webtransport-browser-smoke/vite.config.ts
Normal file
50
Web/game-realtime-webtransport-browser-smoke/vite.config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: Web/game-realtime-webtransport-browser-smoke/vite.config.ts
|
||||
// version: 1
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolve } from "node:path";
|
||||
import { defineConfig, normalizePath } from "vite";
|
||||
|
||||
const appRoot = fileURLToPath(new URL(".", import.meta.url));
|
||||
const repositoryRoot = normalizePath(resolve(appRoot, "../.."));
|
||||
const frontendRoot = normalizePath(resolve(appRoot, "frontend"));
|
||||
const externalBuildRoot = normalizePath(resolve(repositoryRoot, "../builds/sasedev-games/game-realtime-webtransport-browser-smoke"));
|
||||
const wasmRoot = normalizePath(resolve(externalBuildRoot, "wasm"));
|
||||
const wasmModule = normalizePath(resolve(wasmRoot, "game_realtime_webtransport_browser_smoke.js"));
|
||||
const frontendDist = normalizePath(resolve(externalBuildRoot, "dist"));
|
||||
const viteCacheDir = normalizePath(resolve(externalBuildRoot, "vite-cache"));
|
||||
|
||||
export default defineConfig({
|
||||
base: "./",
|
||||
cacheDir: viteCacheDir,
|
||||
clearScreen: false,
|
||||
root: frontendRoot,
|
||||
publicDir: false,
|
||||
input: {
|
||||
main: normalizePath(resolve(frontendRoot, "main.html")),
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@webtransport-browser-smoke-wasm": wasmModule,
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir: frontendDist,
|
||||
emptyOutDir: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
},
|
||||
server: {
|
||||
host: "127.0.0.1",
|
||||
port: 1435,
|
||||
strictPort: true,
|
||||
fs: {
|
||||
allow: [appRoot, repositoryRoot, externalBuildRoot],
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
host: "127.0.0.1",
|
||||
port: 4175,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
# file: crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "game-realtime-webtransport-browser-smoke"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" }
|
||||
game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
game-logging-lib = { path = "../../common/game-logging-lib" }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "time"] }
|
||||
tracing.workspace = true
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,96 @@
|
||||
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/browser.rs
|
||||
// version: 1
|
||||
|
||||
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 CERTIFICATE_HASH_HEX_SIZE: usize = 64;
|
||||
const CERTIFICATE_HASH_SIZE: usize = 32;
|
||||
|
||||
/// Runs one real-browser WebTransport round-trip against the supplied pinned endpoint.
|
||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||
pub async fn run_browser_smoke(endpoint: String, certificate_sha256_hex: String) -> Result<String, wasm_bindgen::JsValue> {
|
||||
let certificate_hash = match decode_certificate_hash(certificate_sha256_hex.as_str()) {
|
||||
Ok(value) => game_realtime_webtransport_lib::WebTransportCertificateHash::from_sha256(value),
|
||||
Err(error) => return Err(wasm_bindgen::JsValue::from_str(error.as_str())),
|
||||
};
|
||||
let transport = game_realtime_webtransport_lib::WebTransportConfig::default()
|
||||
.with_connect_timeout(std::time::Duration::from_secs(5))
|
||||
.with_primary_stream_timeout(std::time::Duration::from_secs(5))
|
||||
.with_send_timeout(std::time::Duration::from_secs(5));
|
||||
let config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) {
|
||||
Ok(value) => value.with_transport_config(transport),
|
||||
Err(error) => return Err(js_transport_error("browser client configuration failed", error)),
|
||||
};
|
||||
let session = match game_realtime_webtransport_lib::connect(&config).await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(js_transport_error("browser client connection failed", error)),
|
||||
};
|
||||
let connection = match session.open_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(js_transport_error("browser primary stream open failed", error)),
|
||||
};
|
||||
let (mut sender, mut receiver) = connection.split();
|
||||
if let Err(error) = sender.send(game_realtime_transport_lib::TransportMessage::new(crate::shared::BROWSER_PAYLOAD.to_vec())).await {
|
||||
return Err(js_transport_error("browser send failed", error));
|
||||
}
|
||||
let received = match receiver.receive().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(js_transport_error("browser receive failed", error)),
|
||||
};
|
||||
if !receive_matches(received, crate::shared::SERVER_PAYLOAD) {
|
||||
return Err(wasm_bindgen::JsValue::from_str("browser did not receive the expected server payload"));
|
||||
}
|
||||
if let Err(error) = sender.close().await {
|
||||
return Err(js_transport_error("browser close failed", error));
|
||||
}
|
||||
let remote_close = match receiver.receive().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(js_transport_error("browser close observation failed", error)),
|
||||
};
|
||||
if remote_close != game_realtime_transport_lib::TransportReceive::Closed {
|
||||
return Err(wasm_bindgen::JsValue::from_str("browser did not observe the server FIN"));
|
||||
}
|
||||
return Ok(String::from("game-realtime-webtransport-browser-smoke: PASS"));
|
||||
}
|
||||
|
||||
fn decode_certificate_hash(value: &str) -> Result<[u8; CERTIFICATE_HASH_SIZE], String> {
|
||||
let encoded = value.as_bytes();
|
||||
if encoded.len() != CERTIFICATE_HASH_HEX_SIZE {
|
||||
return Err(String::from("certificate SHA-256 hash must contain exactly 64 hexadecimal characters"));
|
||||
}
|
||||
let mut decoded = [0_u8; CERTIFICATE_HASH_SIZE];
|
||||
for index in 0..CERTIFICATE_HASH_SIZE {
|
||||
let high = match hex_nibble(encoded[index * 2]) {
|
||||
Some(value) => value,
|
||||
None => return Err(String::from("certificate SHA-256 hash contains a non-hexadecimal character")),
|
||||
};
|
||||
let low = match hex_nibble(encoded[index * 2 + 1]) {
|
||||
Some(value) => value,
|
||||
None => return Err(String::from("certificate SHA-256 hash contains a non-hexadecimal character")),
|
||||
};
|
||||
decoded[index] = (high << 4) | low;
|
||||
}
|
||||
return Ok(decoded);
|
||||
}
|
||||
|
||||
fn hex_nibble(value: u8) -> Option<u8> {
|
||||
return match value {
|
||||
b'0'..=b'9' => Some(value - b'0'),
|
||||
b'a'..=b'f' => Some(value - b'a' + 10),
|
||||
b'A'..=b'F' => Some(value - b'A' + 10),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
fn js_transport_error(context: &str, error: game_realtime_transport_lib::TransportError) -> wasm_bindgen::JsValue {
|
||||
return wasm_bindgen::JsValue::from_str(format!("{context}: {error}").as_str());
|
||||
}
|
||||
|
||||
fn receive_matches(receive: game_realtime_transport_lib::TransportReceive, expected: &[u8]) -> bool {
|
||||
return match receive {
|
||||
game_realtime_transport_lib::TransportReceive::Message(message) => message.as_bytes() == expected,
|
||||
game_realtime_transport_lib::TransportReceive::Closed => false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! WebAssembly adapter for the real-browser WebTransport smoke.
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod browser;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod shared;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
/// Re-export of the browser smoke entry point exposed through wasm-bindgen.
|
||||
pub use self::browser::run_browser_smoke;
|
||||
144
crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
Normal file
144
crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
|
||||
// version: 1
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
//! Native peer for the real-browser WebTransport smoke.
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod shared;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const BROWSER_HOST_URL: &str = "http://127.0.0.1:1435/main.html";
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const BROWSER_SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120);
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const TRACING_TARGET: &str = "games::realtime::webtransport::browser-smoke";
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> std::process::ExitCode {
|
||||
let _logging_guard = match game_logging_lib::init_console_tracing() {
|
||||
std::result::Result::Ok(guard) => guard,
|
||||
std::result::Result::Err(error) => {
|
||||
eprintln!("failed to initialize browser WebTransport smoke tracing: {error}");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
let prepared = match prepare_listener() {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "browser WebTransport smoke preparation failed");
|
||||
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: {error}");
|
||||
return std::process::ExitCode::FAILURE;
|
||||
},
|
||||
};
|
||||
let (mut listener, endpoint, certificate_hash_hex) = prepared;
|
||||
let browser_url = format!("{BROWSER_HOST_URL}?endpoint={endpoint}&sha256={certificate_hash_hex}");
|
||||
tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "browser WebTransport smoke peer ready");
|
||||
println!("game-realtime-webtransport-browser-smoke: ENDPOINT={endpoint}");
|
||||
println!("game-realtime-webtransport-browser-smoke: CERT_SHA256={certificate_hash_hex}");
|
||||
println!("game-realtime-webtransport-browser-smoke: OPEN={browser_url}");
|
||||
let result = tokio::time::timeout(BROWSER_SMOKE_TIMEOUT, serve_browser(&mut listener)).await;
|
||||
return match result {
|
||||
Ok(Ok(())) => {
|
||||
tracing::info!(target: TRACING_TARGET, "browser WebTransport smoke passed");
|
||||
println!("game-realtime-webtransport-browser-smoke: PASS");
|
||||
std::process::ExitCode::SUCCESS
|
||||
},
|
||||
Ok(Err(error)) => {
|
||||
tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "browser WebTransport smoke failed");
|
||||
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: {error}");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
Err(_) => {
|
||||
tracing::error!(target: TRACING_TARGET, timeout_ms = BROWSER_SMOKE_TIMEOUT.as_millis(), "browser WebTransport smoke timed out");
|
||||
eprintln!("game-realtime-webtransport-browser-smoke: FAIL: browser did not complete the smoke before the launcher deadline");
|
||||
std::process::ExitCode::FAILURE
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn prepare_listener() -> Result<(game_realtime_webtransport_lib::WebTransportListener, String, String), String> {
|
||||
let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("loopback identity generation failed: {error}")),
|
||||
};
|
||||
let certificate_hash_hex = encode_hex(identity.certificate_hash().as_bytes());
|
||||
let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0));
|
||||
let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(bind_address, identity);
|
||||
let listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("listener bind failed: {error}")),
|
||||
};
|
||||
let endpoint = format!("https://{}/browser-smoke", listener.local_addr());
|
||||
return Ok((listener, endpoint, certificate_hash_hex));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn serve_browser(listener: &mut game_realtime_webtransport_lib::WebTransportListener) -> Result<(), String> {
|
||||
let session = match listener.accept().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("server accept failed: {error}")),
|
||||
};
|
||||
let connection = match session.accept_primary_connection().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("server primary stream accept failed: {error}")),
|
||||
};
|
||||
let (mut sender, mut receiver) = connection.split();
|
||||
let browser_received = match receiver.receive().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("server receive failed: {error}")),
|
||||
};
|
||||
if !receive_matches(browser_received, shared::BROWSER_PAYLOAD) {
|
||||
return Err(String::from("server did not receive the expected browser payload"));
|
||||
}
|
||||
if let Err(error) = sender.send(game_realtime_transport_lib::TransportMessage::new(shared::SERVER_PAYLOAD.to_vec())).await {
|
||||
return Err(format!("server send failed: {error}"));
|
||||
}
|
||||
let browser_close = match receiver.receive().await {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(format!("server close observation failed: {error}")),
|
||||
};
|
||||
if browser_close != game_realtime_transport_lib::TransportReceive::Closed {
|
||||
return Err(String::from("server did not observe the browser FIN"));
|
||||
}
|
||||
if let Err(error) = sender.close().await {
|
||||
return Err(format!("server close failed: {error}"));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn encode_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut encoded = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
encoded.push(char::from(HEX[usize::from(byte >> 4)]));
|
||||
encoded.push(char::from(HEX[usize::from(byte & 0x0f)]));
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn receive_matches(receive: game_realtime_transport_lib::TransportReceive, expected: &[u8]) -> bool {
|
||||
return match receive {
|
||||
game_realtime_transport_lib::TransportReceive::Message(message) => message.as_bytes() == expected,
|
||||
game_realtime_transport_lib::TransportReceive::Closed => false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// file: crates/apps/game-realtime-webtransport-browser-smoke/src/shared.rs
|
||||
// version: 1
|
||||
|
||||
/// Binary payload sent from the real browser to the native smoke peer.
|
||||
pub(crate) const BROWSER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-browser-client-smoke";
|
||||
/// Binary payload sent from the native smoke peer back to the real browser.
|
||||
pub(crate) const SERVER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-browser-server-smoke";
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: crates/common/game-realtime-webtransport-lib/Cargo.toml
|
||||
# version: 3
|
||||
# version: 4
|
||||
|
||||
[package]
|
||||
name = "game-realtime-webtransport-lib"
|
||||
@@ -21,6 +21,8 @@ tokio = { workspace = true, features = ["time"] }
|
||||
web-transport-quinn = { workspace = true, features = ["ring"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures-util = { workspace = true, features = ["std"] }
|
||||
gloo-timers = { workspace = true, features = ["futures"] }
|
||||
web-transport-wasm.workspace = true
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/README.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# game-realtime-webtransport-lib
|
||||
|
||||
@@ -19,7 +19,7 @@ La frontière fiable disponible couvre désormais :
|
||||
- sélection d'un unique stream bidirectionnel fiable comme chemin realtime principal ;
|
||||
- framing privé `u32` big-endian + payload binaire ;
|
||||
- limite de message configurable, 1 MiB par défaut, vérifiée avant allocation côté réception et avant écriture côté émission ;
|
||||
- deadlines configurables sur le chemin natif pour la connexion, l'ouverture/accept du stream primaire et un envoi complet ;
|
||||
- deadlines configurables sur les chemins natif et navigateur pour la connexion, l'ouverture/accept du stream primaire et un envoi complet ;
|
||||
- adaptation `RealtimeConnection` / `RealtimeSender` / `RealtimeReceiver` ;
|
||||
- FIN propre via `RealtimeSender::close()` ;
|
||||
- reset/STOP_SENDING backend-spécifiques via `WebTransportSender::abort(...)` et `WebTransportReceiver::abort(...)` ;
|
||||
@@ -38,7 +38,7 @@ L'API client garde les mêmes noms de surface que le client natif : `WebTranspor
|
||||
|
||||
Le pin SHA-256 est transmis à `WebTransportOptions.serverCertificateHashes`; aucune variante navigateur sans validation TLS n'est ajoutée. Le framing applicatif reste strictement identique au natif.
|
||||
|
||||
`alpha.6` ferme uniquement la preuve de compilation de ce chemin. La limite de message de `WebTransportConfig` est appliquée immédiatement, mais les deadlines opérationnelles de cette configuration restent une différence explicite : le wrapper navigateur conserve les opérations Web API abandonnées de manière cancel-safe, et la tranche `alpha.7` doit décider puis prouver la politique de timer/runtime avant de déclarer la parité comportementale navigateur. Aucun smoke runtime navigateur n'est attribué à `alpha.6`.
|
||||
À partir de `alpha.7`, les deadlines `connect_timeout`, `primary_stream_timeout` et `send_timeout` sont aussi matérialisées côté navigateur par des timers WASM. Une deadline navigateur doit tenir dans la plage `u32` millisecondes ; une valeur supérieure est rejetée comme `InvalidConfiguration`. Une réception idle reste volontairement sans timeout implicite, comme sur le chemin natif. Le smoke navigateur technique séparé prouve ensuite cette surface avec un vrai navigateur sans fallback WebSocket.
|
||||
|
||||
## TLS de développement
|
||||
|
||||
@@ -69,7 +69,7 @@ one WebTransport session
|
||||
|
||||
`WebTransportConfig::default()` conserve la baseline de 1 MiB par message. La limite peut être réduite ou augmentée tant qu'elle reste strictement positive et représentable dans le champ de longueur `u32` du framing.
|
||||
|
||||
Sur le chemin natif, les deadlines configurables couvrent :
|
||||
Sur les chemins natif et navigateur, les deadlines configurables couvrent :
|
||||
|
||||
- connexion client et réponse finale à une requête WebTransport déjà surfacée côté serveur ;
|
||||
- ouverture ou accept du stream bidirectionnel principal ;
|
||||
@@ -77,7 +77,7 @@ Sur le chemin natif, les deadlines configurables couvrent :
|
||||
|
||||
L'attente d'un nouveau pair sur le listener reste volontairement non bornée : un serveur inactif ne doit pas produire périodiquement une erreur uniquement parce qu'aucun client ne se présente.
|
||||
|
||||
QUIC applique sa propre flow-control. Le backend natif n'ajoute pas une seconde file applicative : si un envoi reste bloqué par flow-control/réseau au-delà de `send_timeout`, l'opération retourne `TransportErrorKind::Timeout` et le stream est reset afin qu'une frame partiellement transmise ne puisse pas être suivie d'une nouvelle frame invalide. Le chemin navigateur conserve la même politique de reset sur cancellation, mais son timer `send_timeout` reste explicitement différé à `alpha.7`.
|
||||
QUIC applique sa propre flow-control. Le backend n'ajoute pas une seconde file applicative : si un envoi reste bloqué par flow-control/réseau au-delà de `send_timeout`, l'opération retourne `TransportErrorKind::Timeout` et le stream est reset afin qu'une frame partiellement transmise ne puisse pas être suivie d'une nouvelle frame invalide. Côté navigateur, le timer est porté par `gloo-timers` et la future WebTransport abandonnée reste traitée selon la sémantique cancel-safe du wrapper amont.
|
||||
|
||||
## Lifecycle, abort et cancellation
|
||||
|
||||
@@ -103,7 +103,7 @@ Le backend distingue notamment :
|
||||
- fermeture de session WebTransport explicite -> `Closed` ;
|
||||
- erreur de session/connexion non classée comme fermeture propre -> `Io` ;
|
||||
- reset/stop invalide ou framing tronqué -> `Protocol` ;
|
||||
- deadline dépassée -> `Timeout` sur le chemin natif ; la matérialisation des timers navigateur reste différée à `alpha.7`.
|
||||
- deadline dépassée -> `Timeout` sur les chemins natif et navigateur.
|
||||
|
||||
Une longueur entrante hors limite ou un framing tronqué provoque aussi l'arrêt de la direction de réception afin d'éviter de poursuivre sur un flux désynchronisé.
|
||||
|
||||
@@ -113,8 +113,7 @@ La crate ne possède toujours pas :
|
||||
|
||||
- d'API datagram transport-neutral ;
|
||||
- de serveur WebTransport WASM ;
|
||||
- de preuve runtime navigateur ;
|
||||
- de fallback WebSocket ;
|
||||
- de fallback WebSocket dans le backend ;
|
||||
- de benchmark WebSocket/WebTransport.
|
||||
|
||||
Ces responsabilités restent réservées aux tranches suivantes du plan `0.3.5`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/common/game-realtime-webtransport-lib/USAGE.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# Utilisation de game-realtime-webtransport-lib
|
||||
|
||||
@@ -7,7 +7,7 @@ Ce guide décrit les chemins fiables natif et client navigateur/WASM exposés pa
|
||||
|
||||
## Configuration transport
|
||||
|
||||
`WebTransportConfig` porte les limites et deadlines du chemin fiable. La configuration par défaut garde une limite de 1 MiB par message. Les deadlines sont appliquées par le chemin natif ; le client navigateur les valide mais leur matérialisation par timer reste différée à `alpha.7`.
|
||||
`WebTransportConfig` porte les limites et deadlines du chemin fiable. La configuration par défaut garde une limite de 1 MiB par message. Les deadlines sont appliquées sur les chemins natif et navigateur pour la connexion, la sélection du stream primaire et chaque envoi complet.
|
||||
|
||||
Exemple de configuration plus stricte :
|
||||
|
||||
@@ -19,7 +19,7 @@ let transport = game_realtime_webtransport_lib::WebTransportConfig::default()
|
||||
.with_send_timeout(std::time::Duration::from_secs(2));
|
||||
```
|
||||
|
||||
La validation effective se fait lors du bind serveur ou de la connexion client. Une limite nulle, une limite non représentable en `u32` ou une deadline nulle est rejetée comme `InvalidConfiguration`, y compris côté navigateur même lorsque le timer correspondant n'est pas encore matérialisé.
|
||||
La validation effective se fait lors du bind serveur ou de la connexion client. Une limite nulle, une limite non représentable en `u32` ou une deadline nulle est rejetée comme `InvalidConfiguration`. Côté navigateur, les deadlines doivent également tenir dans la plage `u32` millisecondes imposée par le timer WASM.
|
||||
|
||||
## Serveur natif
|
||||
|
||||
@@ -102,9 +102,9 @@ let connection = match session.open_primary_connection().await {
|
||||
};
|
||||
```
|
||||
|
||||
Le build workspace fournit `web_sys_unstable_apis` uniquement à `wasm32-unknown-unknown`. Le hash SHA-256 est transmis au navigateur comme `serverCertificateHashes`. La limite de message configurée est appliquée au framing WASM. Les deadlines `connect_timeout`, `primary_stream_timeout` et `send_timeout` restent validées mais ne sont pas encore appliquées par un timer navigateur dans la tranche de compilation ; cette politique est fermée avec le smoke runtime de la tranche suivante.
|
||||
Le build workspace fournit `web_sys_unstable_apis` uniquement à `wasm32-unknown-unknown`. Le hash SHA-256 est transmis au navigateur comme `serverCertificateHashes`. La limite de message configurée est appliquée au framing WASM. `connect_timeout`, `primary_stream_timeout` et `send_timeout` sont réalisés par des timers WASM ; une expiration retourne `TransportErrorKind::Timeout`, et un send expiré reset le stream comme sur le chemin natif.
|
||||
|
||||
Aucun `wasm-bindgen` frontend ou host Vite n'est requis pour simplement vérifier la compilation de la bibliothèque.
|
||||
Le host Vite et l'adapter `wasm-bindgen` de `game-realtime-webtransport-browser-smoke` servent uniquement de preuve runtime. Ils ne sont pas nécessaires à un consommateur qui intègre déjà la bibliothèque dans son propre frontend.
|
||||
|
||||
## Contrat realtime
|
||||
|
||||
@@ -130,7 +130,7 @@ if let Err(error) = game_realtime_transport_lib::RealtimeSender::close(&mut send
|
||||
|
||||
Le backend encode chaque `TransportMessage` sous la forme `u32` big-endian + payload. Le consommateur ne doit pas reproduire ce framing lui-même.
|
||||
|
||||
La flow-control QUIC est respectée naturellement par l'écriture asynchrone. Sur le chemin natif, un send qui dépasse sa deadline est considéré terminal : le stream est reset et le même sender ne doit pas être réutilisé. Sur le chemin navigateur de `alpha.6`, une cancellation du send reste terminale et reset le stream, mais le timer `send_timeout` est différé à `alpha.7`.
|
||||
La flow-control QUIC est respectée naturellement par l'écriture asynchrone. Sur les chemins natif et navigateur, un send qui dépasse sa deadline est considéré terminal : le stream est reset et le même sender ne doit pas être réutilisé.
|
||||
|
||||
## Fermeture et abort
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// file: crates/common/game-realtime-webtransport-lib/src/webtransport_wasm.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
const CERTIFICATE_HASH_SIZE: usize = 32;
|
||||
const FRAME_PROTOCOL_ERROR_CODE: u32 = 0x10;
|
||||
const FRAME_TOO_LARGE_ERROR_CODE: u32 = 0x11;
|
||||
const PRIMARY_FRAME_HEADER_SIZE: usize = 4;
|
||||
const SEND_FAILURE_ERROR_CODE: u32 = 0x12;
|
||||
const SEND_TIMEOUT_ERROR_CODE: u32 = 0x13;
|
||||
const STREAM_CANCELLED_ERROR_CODE: u32 = 0x14;
|
||||
const TRACING_TARGET: &str = "games::realtime::webtransport";
|
||||
|
||||
@@ -54,8 +56,7 @@ impl WebTransportClientConfig {
|
||||
|
||||
/// Returns a copy with explicit reliable-path limits and deadlines.
|
||||
///
|
||||
/// The browser compile path applies the message-size limit immediately. Browser-side operation deadlines are
|
||||
/// validated here but remain a runtime-interoperability concern for the dedicated browser smoke tranche.
|
||||
/// The browser path applies the message-size limit and operation deadlines to connect, primary-stream open and send.
|
||||
#[must_use]
|
||||
pub fn with_transport_config(mut self, transport: crate::WebTransportConfig) -> Self {
|
||||
self.transport = transport;
|
||||
@@ -94,13 +95,24 @@ impl WebTransportSession {
|
||||
|
||||
/// 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 {
|
||||
let timeout = self.transport.primary_stream_timeout();
|
||||
let timeout_millis = match browser_timeout_millis(timeout, "primary_stream_timeout") {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let opened = await_with_timeout(self.inner.open_bi(), timeout_millis).await;
|
||||
let (sender, receiver) = match opened {
|
||||
Some(Ok(value)) => value,
|
||||
Some(Err(error)) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Protocol, error.to_string());
|
||||
tracing::warn!(target: TRACING_TARGET, detail = mapped.detail(), "browser WebTransport primary bidirectional stream open failed");
|
||||
return Err(mapped);
|
||||
},
|
||||
None => {
|
||||
let mapped = timeout_error("browser WebTransport primary bidirectional stream open", timeout);
|
||||
tracing::warn!(target: TRACING_TARGET, timeout_ms = timeout.as_millis(), "browser WebTransport primary bidirectional stream open timed out");
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::debug!(target: TRACING_TARGET, endpoint = self.inner.url().as_str(), "browser WebTransport primary bidirectional stream opened");
|
||||
return Ok(WebTransportConnection::new(self.inner, sender, receiver, self.transport));
|
||||
@@ -143,6 +155,7 @@ impl game_realtime_transport_lib::RealtimeConnection for WebTransportConnection
|
||||
inner: self.sender,
|
||||
_session: self.session,
|
||||
max_message_size: self.transport.max_message_size(),
|
||||
send_timeout: self.transport.send_timeout(),
|
||||
terminal: false,
|
||||
},
|
||||
crate::WebTransportReceiver {
|
||||
@@ -316,6 +329,7 @@ pub struct WebTransportSender {
|
||||
inner: web_transport_wasm::SendStream,
|
||||
_session: web_transport_wasm::Session,
|
||||
max_message_size: usize,
|
||||
send_timeout: std::time::Duration,
|
||||
terminal: bool,
|
||||
}
|
||||
|
||||
@@ -384,16 +398,33 @@ impl game_realtime_transport_lib::RealtimeSender for WebTransportSender {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let send_timeout = self.send_timeout;
|
||||
let timeout_millis = match browser_timeout_millis(send_timeout, "send_timeout") {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let mut guard = SendOperationGuard::new(&mut self.inner, &mut self.terminal);
|
||||
if let Err(error) = guard.write_frame(&frame_header, message.as_bytes()).await {
|
||||
let mapped = map_write_error(error);
|
||||
guard.abort();
|
||||
tracing::warn!(target: TRACING_TARGET, kind = %mapped.kind(), detail = mapped.detail(), "browser WebTransport framed send failed");
|
||||
return Err(mapped);
|
||||
}
|
||||
guard.disarm();
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "framed browser WebTransport payload sent");
|
||||
return Ok(());
|
||||
let operation = guard.write_frame(&frame_header, message.as_bytes());
|
||||
let result = await_with_timeout(operation, timeout_millis).await;
|
||||
return match result {
|
||||
Some(Ok(())) => {
|
||||
guard.complete();
|
||||
tracing::trace!(target: TRACING_TARGET, payload_len = payload_len, "framed browser WebTransport payload sent");
|
||||
Ok(())
|
||||
},
|
||||
Some(Err(error)) => {
|
||||
guard.abort(SEND_FAILURE_ERROR_CODE);
|
||||
let mapped = map_write_error(error);
|
||||
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, kind = %mapped.kind(), detail = mapped.detail(), "browser WebTransport framed send failed");
|
||||
Err(mapped)
|
||||
},
|
||||
None => {
|
||||
guard.abort(SEND_TIMEOUT_ERROR_CODE);
|
||||
let mapped = timeout_error("browser WebTransport framed send", send_timeout);
|
||||
tracing::warn!(target: TRACING_TARGET, payload_len = payload_len, timeout_ms = send_timeout.as_millis(), "browser WebTransport framed send timed out under flow control/backpressure");
|
||||
Err(mapped)
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -409,15 +440,15 @@ impl<'a> SendOperationGuard<'a> {
|
||||
return Self { inner, terminal, armed: true };
|
||||
}
|
||||
|
||||
fn abort(&mut self) {
|
||||
fn abort(&mut self, code: u32) {
|
||||
if self.armed {
|
||||
self.inner.reset(STREAM_CANCELLED_ERROR_CODE);
|
||||
self.inner.reset(code);
|
||||
*self.terminal = true;
|
||||
self.armed = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn disarm(&mut self) {
|
||||
fn complete(&mut self) {
|
||||
self.armed = false;
|
||||
}
|
||||
|
||||
@@ -438,7 +469,7 @@ impl<'a> SendOperationGuard<'a> {
|
||||
|
||||
impl Drop for SendOperationGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.abort();
|
||||
self.abort(STREAM_CANCELLED_ERROR_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,13 +478,22 @@ pub async fn connect(config: &WebTransportClientConfig) -> Result<WebTransportSe
|
||||
if let Err(error) = config.transport.validate() {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = validate_browser_deadlines(config.transport) {
|
||||
return Err(error);
|
||||
}
|
||||
let client = web_transport_wasm::ClientBuilder::new()
|
||||
.with_pooling(false)
|
||||
.with_unreliable(true)
|
||||
.with_server_certificate_hashes(vec![config.certificate_hash.as_bytes().to_vec()]);
|
||||
let session = match client.connect(config.endpoint.clone()).await {
|
||||
let timeout = config.transport.connect_timeout();
|
||||
let timeout_millis = match browser_timeout_millis(timeout, "connect_timeout") {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let connected = await_with_timeout(client.connect(config.endpoint.clone()), timeout_millis).await;
|
||||
let session = match connected {
|
||||
Some(Ok(value)) => value,
|
||||
Some(Err(error)) => {
|
||||
let mapped = transport_error(game_realtime_transport_lib::TransportErrorKind::Connect, error.to_string());
|
||||
tracing::warn!(
|
||||
target: TRACING_TARGET,
|
||||
@@ -463,11 +503,56 @@ pub async fn connect(config: &WebTransportClientConfig) -> Result<WebTransportSe
|
||||
);
|
||||
return Err(mapped);
|
||||
},
|
||||
None => {
|
||||
let mapped = timeout_error("browser WebTransport client connect", timeout);
|
||||
tracing::warn!(target: TRACING_TARGET, endpoint = config.endpoint.as_str(), timeout_ms = timeout.as_millis(), "browser WebTransport client connection timed out");
|
||||
return Err(mapped);
|
||||
},
|
||||
};
|
||||
tracing::info!(target: TRACING_TARGET, endpoint = config.endpoint.as_str(), "browser WebTransport client connected");
|
||||
return Ok(WebTransportSession::new(session, config.transport));
|
||||
}
|
||||
|
||||
async fn await_with_timeout<F>(operation: F, timeout_millis: u32) -> Option<F::Output>
|
||||
where
|
||||
F: core::future::Future,
|
||||
{
|
||||
let operation = Box::pin(operation);
|
||||
let timeout = Box::pin(gloo_timers::future::TimeoutFuture::new(timeout_millis));
|
||||
return match futures_util::future::select(operation, timeout).await {
|
||||
futures_util::future::Either::Left((output, _)) => Some(output),
|
||||
futures_util::future::Either::Right(((), _)) => None,
|
||||
};
|
||||
}
|
||||
|
||||
fn browser_timeout_millis(duration: std::time::Duration, name: &str) -> Result<u32, game_realtime_transport_lib::TransportError> {
|
||||
return match u32::try_from(duration.as_millis()) {
|
||||
Ok(value) if value > 0 => Ok(value),
|
||||
Ok(_) => Err(invalid_configuration(format!("{name} must resolve to at least one browser timer millisecond"))),
|
||||
Err(_) => Err(invalid_configuration(format!("{name} exceeds the browser timer range of u32 milliseconds"))),
|
||||
};
|
||||
}
|
||||
|
||||
fn timeout_error(operation: &str, timeout: std::time::Duration) -> game_realtime_transport_lib::TransportError {
|
||||
return transport_error(
|
||||
game_realtime_transport_lib::TransportErrorKind::Timeout,
|
||||
format!("{operation} exceeded configured deadline of {} ms", timeout.as_millis()),
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_browser_deadlines(config: crate::WebTransportConfig) -> Result<(), game_realtime_transport_lib::TransportError> {
|
||||
if let Err(error) = browser_timeout_millis(config.connect_timeout(), "connect_timeout") {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = browser_timeout_millis(config.primary_stream_timeout(), "primary_stream_timeout") {
|
||||
return Err(error);
|
||||
}
|
||||
if let Err(error) = browser_timeout_millis(config.send_timeout(), "send_timeout") {
|
||||
return Err(error);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn frame_header(payload_len: usize, max_message_size: usize) -> Result<[u8; PRIMARY_FRAME_HEADER_SIZE], game_realtime_transport_lib::TransportError> {
|
||||
if payload_len > max_message_size {
|
||||
return Err(message_too_large(payload_len, max_message_size));
|
||||
|
||||
243
deltas/0.3.5/alpha.7.md
Normal file
243
deltas/0.3.5/alpha.7.md
Normal file
@@ -0,0 +1,243 @@
|
||||
<!-- file: deltas/0.3.5/alpha.7.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Delta 0.3.5-alpha.7
|
||||
|
||||
## Base requise
|
||||
|
||||
`0.3.5-alpha.6`, validée par l'utilisateur le 2026-09-22 avec fmt, audits, `cargo check`, Clippy strict, les sept tests du contrat realtime commun, les vingt tests WebTransport, le smoke natif `PASS`, puis check/Clippy/build réels de `game-realtime-webtransport-lib` en `wasm32-unknown-unknown`.
|
||||
|
||||
Le graphe WASM confirmé ne tire ni `web-transport-quinn`, ni rcgen, ni Tokio. La preuve est enregistrée dans `history/0.3.5/alpha.6.md`.
|
||||
|
||||
## Objectif
|
||||
|
||||
Fermer l'interop navigateur réelle du POC WebTransport sans introduire encore le fallback WebSocket :
|
||||
|
||||
- appliquer réellement les deadlines `WebTransportConfig` côté browser/WASM ;
|
||||
- charger le client Rust/WASM dans un host Vite technique isolé ;
|
||||
- connecter un navigateur récent à un serveur Rust WebTransport local ;
|
||||
- transmettre explicitement le hash SHA-256 du certificat court P-256 ;
|
||||
- effectuer un round-trip binaire bidirectionnel via le stream primaire ;
|
||||
- fermer proprement les deux directions ;
|
||||
- produire un verdict `PASS` reproductible côté navigateur et côté serveur.
|
||||
|
||||
## Version
|
||||
|
||||
La version workspace passe à :
|
||||
|
||||
```text
|
||||
0.3.5-alpha.7
|
||||
```
|
||||
|
||||
## Deadlines navigateur
|
||||
|
||||
Le target `wasm32` de `game-realtime-webtransport-lib` ajoute uniquement :
|
||||
|
||||
```text
|
||||
gloo-timers 0.4.0
|
||||
futures-util 0.3.34 avec feature std
|
||||
```
|
||||
|
||||
Les dépendances restent target-specific et ne changent pas le backend natif.
|
||||
|
||||
Les opérations navigateur suivantes sont maintenant bornées :
|
||||
|
||||
```text
|
||||
connect_timeout
|
||||
primary_stream_timeout
|
||||
send_timeout
|
||||
```
|
||||
|
||||
La politique reste identique au natif :
|
||||
|
||||
- `connect` expiré -> `TransportErrorKind::Timeout` ;
|
||||
- ouverture du stream primaire expirée -> `Timeout` ;
|
||||
- send expiré -> `Timeout` puis reset terminal du stream ;
|
||||
- `receive()` idle reste sans timeout implicite ;
|
||||
- cancellation de `receive()` reste reprenable grâce au parseur incrémental.
|
||||
|
||||
Le timer browser prend des millisecondes `u32`. Une deadline qui devient zéro après conversion ou dépasse cette plage est rejetée comme `InvalidConfiguration` avant établissement.
|
||||
|
||||
## Smoke navigateur
|
||||
|
||||
Nouveau package technique :
|
||||
|
||||
```text
|
||||
crates/apps/game-realtime-webtransport-browser-smoke
|
||||
```
|
||||
|
||||
Le même package possède :
|
||||
|
||||
- un binary natif qui génère l'identité loopback P-256 en mémoire, bind un serveur WebTransport sur port éphémère, affiche endpoint + SHA-256 et attend exactement le round-trip navigateur ;
|
||||
- une lib `cdylib`/`rlib` dont l'export `run_browser_smoke(...)` compile en WASM et consomme réellement `game-realtime-webtransport-lib`.
|
||||
|
||||
Nouveau host direct :
|
||||
|
||||
```text
|
||||
Web/game-realtime-webtransport-browser-smoke
|
||||
```
|
||||
|
||||
Il utilise Vite + TypeScript, génère les bindings avec `wasm-bindgen` hors du repository et sert la page sur :
|
||||
|
||||
```text
|
||||
http://127.0.0.1:1435/main.html
|
||||
```
|
||||
|
||||
Le loopback HTTP est un secure context de navigateur. La page vérifie néanmoins `window.isSecureContext` et la présence de `WebTransport` avant d'exécuter le WASM.
|
||||
|
||||
Le transport cible toujours :
|
||||
|
||||
```text
|
||||
https://127.0.0.1:<port-ephemere>/browser-smoke
|
||||
```
|
||||
|
||||
avec `serverCertificateHashes` SHA-256 ; aucune vérification TLS n'est désactivée.
|
||||
|
||||
## Round-trip prouvé par le smoke
|
||||
|
||||
La séquence attendue est :
|
||||
|
||||
```text
|
||||
browser WASM
|
||||
-> connect WebTransport avec pin SHA-256
|
||||
-> open primary bidirectional stream
|
||||
-> send browser binary payload
|
||||
native Rust peer
|
||||
-> receive exact payload
|
||||
-> send server binary payload
|
||||
browser WASM
|
||||
-> receive exact payload
|
||||
-> FIN send direction
|
||||
native Rust peer
|
||||
-> observe Closed
|
||||
-> FIN send direction
|
||||
browser WASM
|
||||
-> observe Closed
|
||||
```
|
||||
|
||||
Le navigateur doit afficher :
|
||||
|
||||
```text
|
||||
game-realtime-webtransport-browser-smoke: PASS
|
||||
```
|
||||
|
||||
et le binary serveur doit ensuite terminer avec la même ligne.
|
||||
|
||||
Aucun fallback WebSocket n'existe dans cette preuve ; un échec WebTransport doit rester visible.
|
||||
|
||||
## Audit de distribution
|
||||
|
||||
`scripts/audit_distribution_layout.py` possède désormais le host technique et son adapter Rust. Il vérifie notamment :
|
||||
|
||||
- génération WASM hors repository ;
|
||||
- host Vite strictement loopback sur le port 1435 ;
|
||||
- contrôle secure-context/WebTransport côté page ;
|
||||
- appel réel à `run_browser_smoke(...)` ;
|
||||
- dépendance de l'adapter vers `game-realtime-webtransport-lib` ;
|
||||
- verdict `PASS` déterministe du peer natif.
|
||||
|
||||
## Fichiers modifiés
|
||||
|
||||
```text
|
||||
Cargo.toml
|
||||
README.md
|
||||
crates/common/game-realtime-webtransport-lib/Cargo.toml
|
||||
crates/common/game-realtime-webtransport-lib/README.md
|
||||
crates/common/game-realtime-webtransport-lib/USAGE.md
|
||||
crates/common/game-realtime-webtransport-lib/src/webtransport_wasm.rs
|
||||
docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md
|
||||
scripts/audit_distribution_layout.py
|
||||
```
|
||||
|
||||
Nouveaux fichiers :
|
||||
|
||||
```text
|
||||
crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml
|
||||
crates/apps/game-realtime-webtransport-browser-smoke/src/browser.rs
|
||||
crates/apps/game-realtime-webtransport-browser-smoke/src/lib.rs
|
||||
crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs
|
||||
crates/apps/game-realtime-webtransport-browser-smoke/src/shared.rs
|
||||
Web/game-realtime-webtransport-browser-smoke/package.json
|
||||
Web/game-realtime-webtransport-browser-smoke/tsconfig.json
|
||||
Web/game-realtime-webtransport-browser-smoke/vite.config.ts
|
||||
Web/game-realtime-webtransport-browser-smoke/frontend/main.html
|
||||
Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts
|
||||
deltas/0.3.5/alpha.7.md
|
||||
history/0.3.5/alpha.6.md
|
||||
```
|
||||
|
||||
## Validation attendue
|
||||
|
||||
Gate Rust native de non-régression :
|
||||
|
||||
```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 run -p game-realtime-webtransport-smoke
|
||||
```
|
||||
|
||||
Gate WASM :
|
||||
|
||||
```bash
|
||||
cargo check -p game-realtime-webtransport-lib --target wasm32-unknown-unknown
|
||||
cargo clippy -p game-realtime-webtransport-lib --target wasm32-unknown-unknown --lib -- -D warnings
|
||||
cargo build -p game-realtime-webtransport-lib --target wasm32-unknown-unknown
|
||||
|
||||
cargo check -p game-realtime-webtransport-browser-smoke
|
||||
cargo clippy -p game-realtime-webtransport-browser-smoke --all-targets --all-features -- -D warnings
|
||||
cargo build -p game-realtime-webtransport-browser-smoke --lib --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
Build du host Vite direct :
|
||||
|
||||
```bash
|
||||
(cd Web/game-realtime-webtransport-browser-smoke && npm install && npm run build)
|
||||
```
|
||||
|
||||
Smoke runtime réel, avec deux terminaux :
|
||||
|
||||
Terminal A :
|
||||
|
||||
```bash
|
||||
cargo run -p game-realtime-webtransport-browser-smoke
|
||||
```
|
||||
|
||||
Le launcher affiche une ligne `OPEN=...` contenant l'endpoint éphémère et le hash SHA-256.
|
||||
|
||||
Terminal B :
|
||||
|
||||
```bash
|
||||
(cd Web/game-realtime-webtransport-browser-smoke && npm run dev)
|
||||
```
|
||||
|
||||
Ouvrir dans un navigateur récent l'URL `OPEN=...` imprimée par le Terminal A. Si le navigateur demande l'autorisation d'accès loopback/local, l'accorder pour cette preuve locale.
|
||||
|
||||
Résultat attendu dans la page puis dans le Terminal A :
|
||||
|
||||
```text
|
||||
game-realtime-webtransport-browser-smoke: PASS
|
||||
```
|
||||
|
||||
Vérifier enfin le graphe cible :
|
||||
|
||||
```bash
|
||||
cargo tree -p game-realtime-webtransport-browser-smoke --target wasm32-unknown-unknown --edges normal
|
||||
cargo tree -i game-realtime-webtransport-lib --workspace --edges normal
|
||||
cargo tree -i gloo-timers --workspace --target wasm32-unknown-unknown --edges normal
|
||||
```
|
||||
|
||||
Le backend navigateur doit rester indépendant de Quinn/rcgen/Tokio ; `gloo-timers` ne doit apparaître que sur la cible WASM.
|
||||
|
||||
## Après validation
|
||||
|
||||
Créer `history/0.3.5/alpha.7.md` à partir des sorties réellement fournies, puis ouvrir `0.3.5-alpha.8` pour le fallback WebSocket au niveau composition avec branches WebTransport, fallback classifié et erreur non-fallback visible.
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
|
||||
# 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`, son correctif de validation `0.3.5-alpha.2.fix.1`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse validée par `0.3.5-alpha.4.fix.1`, le smoke natif validé de `0.3.5-alpha.5` et le chemin client WASM candidat de `0.3.5-alpha.6`.
|
||||
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`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse validée par `0.3.5-alpha.4.fix.1`, le smoke natif validé de `0.3.5-alpha.5`, le chemin client WASM validé de `0.3.5-alpha.6` et le smoke navigateur candidat de `0.3.5-alpha.7`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -489,22 +489,29 @@ Tranche candidate matérialisée avec :
|
||||
- FIN, reset/STOP, cancellation de send terminale et réception incrémentale conservés sur le chemin navigateur ;
|
||||
- aucun changement Snake/Reflex et aucun host frontend.
|
||||
|
||||
Différence explicitement ouverte pour `alpha.7` : les deadlines `WebTransportConfig` sont validées côté WASM mais aucun timer navigateur n'est encore appliqué. Le wrapper amont conserve les opérations Web API abandonnées de manière cancel-safe ; la preuve runtime doit fixer la politique de timer avant de déclarer la parité comportementale.
|
||||
La gate utilisateur du 2026-09-22 a confirmé le host natif inchangé, les sept tests du contrat commun, les vingt tests WebTransport, le smoke natif `PASS`, puis `cargo check`, Clippy `--lib` et build réel de `game-realtime-webtransport-lib` sur `wasm32-unknown-unknown`. Le graphe WASM ne tire ni Quinn, ni rcgen, ni Tokio. La preuve est conservée dans `history/0.3.5/alpha.6.md`.
|
||||
|
||||
La gate de cette tranche doit prouver le host natif inchangé puis `cargo check`, Clippy `--lib` et build réel de `game-realtime-webtransport-lib` sur `wasm32-unknown-unknown`. Si la famille `web-transport` échoue à cette gate, évaluer `wtransport` serveur + API navigateur directe avant de conclure.
|
||||
Différence fermée dans `alpha.7` : les deadlines `WebTransportConfig` sont matérialisées côté navigateur avec un timer WASM. `connect_timeout`, `primary_stream_timeout` et `send_timeout` sont bornés ; un send expiré reset le stream comme en natif. `receive()` reste volontairement sans timeout d'idle implicite. Les durées navigateur doivent tenir dans `u32` millisecondes, plage du timer retenu, sinon la configuration est refusée avant établissement.
|
||||
|
||||
### `0.3.5-alpha.7` — interop navigateur et TLS local
|
||||
|
||||
- fixer puis prouver la politique de deadlines navigateur réellement nécessaire ;
|
||||
- host technique minimal si nécessaire ;
|
||||
- navigateur récent -> serveur Rust WebTransport ;
|
||||
- hash certificat W3C ;
|
||||
- round-trip binaire ;
|
||||
- close propre ;
|
||||
- smoke documenté et reproductible ;
|
||||
- aucune désactivation permanente de TLS.
|
||||
Tranche candidate matérialisée avec :
|
||||
|
||||
Cette tranche est volontairement séparée de `alpha.6` car certificat, serveur local et browser tooling constituent un risque opérationnel distinct.
|
||||
- `gloo-timers` target-specific WASM pour appliquer les deadlines sans runtime Tokio navigateur ;
|
||||
- `game-realtime-webtransport-browser-smoke`, package technique avec pair serveur natif et adapter `cdylib` WASM ;
|
||||
- host direct `Web/game-realtime-webtransport-browser-smoke` en Vite/TypeScript sur `127.0.0.1:1435` ;
|
||||
- page loopback vérifiant `window.isSecureContext` et la présence de WebTransport avant d'appeler le Rust/WASM ;
|
||||
- serveur Rust sur port QUIC éphémère, certificat P-256 court en mémoire et SHA-256 imprimé explicitement ;
|
||||
- endpoint/hash passés au frontend par query string ou saisie manuelle ;
|
||||
- navigateur -> serveur Rust WebTransport sans fallback WebSocket ;
|
||||
- round-trip binaire dans les deux sens via le stream primaire et `RealtimeConnection` ;
|
||||
- FIN navigateur observé côté serveur, puis FIN serveur observé côté navigateur ;
|
||||
- verdict `game-realtime-webtransport-browser-smoke: PASS` des deux côtés ;
|
||||
- aucune désactivation de TLS et aucun secret/certificat versionné.
|
||||
|
||||
La page Vite peut rester servie en HTTP sur loopback : `127.0.0.1` est un origin potentiellement fiable/secure context dans les navigateurs conformes. Le transport lui-même reste impérativement `https://` et utilise `serverCertificateHashes` avec le certificat X.509v3 court ECDSA P-256 déjà produit par le backend.
|
||||
|
||||
Cette tranche reste volontairement séparée de `alpha.8` : le smoke WebTransport navigateur ne doit contenir aucun fallback silencieux. La sélection/fallback WebSocket est prouvée ensuite au niveau composition.
|
||||
|
||||
### `0.3.5-alpha.8` — fallback WebSocket au niveau composition
|
||||
|
||||
|
||||
93
history/0.3.5/alpha.6.md
Normal file
93
history/0.3.5/alpha.6.md
Normal file
@@ -0,0 +1,93 @@
|
||||
<!-- file: history/0.3.5/alpha.6.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# Historique 0.3.5-alpha.6
|
||||
|
||||
## Statut
|
||||
|
||||
`0.3.5-alpha.6` a été validée par l'utilisateur le 2026-09-22. Le chemin client WebTransport Rust compile donc réellement en `wasm32-unknown-unknown`, tandis que le backend natif et son smoke hors harness restent propres.
|
||||
|
||||
La suite peut ouvrir `0.3.5-alpha.7` pour la preuve runtime dans un vrai navigateur et la fermeture des deadlines navigateur sans réouvrir l'architecture native.
|
||||
|
||||
## Gates statiques et natives
|
||||
|
||||
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), 296 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
|
||||
```
|
||||
|
||||
## Tests realtime et smoke natif
|
||||
|
||||
Le contrat commun reste entièrement propre :
|
||||
|
||||
```text
|
||||
game-realtime-transport-lib: 7 passed; 0 failed
|
||||
```
|
||||
|
||||
La suite WebTransport reste entièrement propre :
|
||||
|
||||
```text
|
||||
unit tests: 11 passed; 0 failed
|
||||
establishment integration tests: 2 passed; 0 failed
|
||||
realtime_connection integration tests: 1 passed; 0 failed
|
||||
robustness integration tests: 6 passed; 0 failed
|
||||
```
|
||||
|
||||
Soit vingt tests WebTransport verts.
|
||||
|
||||
Le smoke natif :
|
||||
|
||||
```bash
|
||||
cargo run -p game-realtime-webtransport-smoke
|
||||
```
|
||||
|
||||
se termine toujours par :
|
||||
|
||||
```text
|
||||
game-realtime-webtransport-smoke: PASS
|
||||
```
|
||||
|
||||
## Gate WASM
|
||||
|
||||
Les trois commandes ciblées ont terminé proprement :
|
||||
|
||||
```text
|
||||
cargo check -p game-realtime-webtransport-lib --target wasm32-unknown-unknown
|
||||
cargo clippy -p game-realtime-webtransport-lib --target wasm32-unknown-unknown --lib -- -D warnings
|
||||
cargo build -p game-realtime-webtransport-lib --target wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
`web-transport-wasm 0.6.0` a été résolu et construit avec les bindings `web-sys`/`wasm-bindgen` attendus.
|
||||
|
||||
Le graphe direct WASM de `game-realtime-webtransport-lib` contient le contrat commun, tracing, URL et `web-transport-wasm`; il ne contient ni `web-transport-quinn`, ni rcgen, ni Tokio. Le graphe inverse confirme :
|
||||
|
||||
```text
|
||||
web-transport-wasm v0.6.0
|
||||
└── game-realtime-webtransport-lib v0.3.5-alpha.6
|
||||
└── game-realtime-webtransport-smoke v0.3.5-alpha.6
|
||||
```
|
||||
|
||||
Le smoke natif apparaît comme consommateur workspace du backend mais sa propre compilation native ne transforme pas le chemin WASM en backend Quinn.
|
||||
|
||||
## Conséquence
|
||||
|
||||
`alpha.7` peut maintenant ajouter la preuve navigateur réelle : host Vite technique isolé, adapter Rust/WASM, serveur Rust local avec certificat court P-256 et hash SHA-256 explicite, round-trip binaire, fermeture propre et deadlines navigateur matérialisées. Le fallback WebSocket reste réservé à `alpha.8`.
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_distribution_layout.py
|
||||
# version: 18
|
||||
# version: 19
|
||||
|
||||
"""Audit the static distribution layout expected by supported POCs."""
|
||||
|
||||
@@ -25,6 +25,9 @@ FORBIDDEN_PATHS = (
|
||||
|
||||
REQUIRED_PATHS = (
|
||||
"crates/apps/game-realtime-websocket-smoke/Cargo.toml",
|
||||
"crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml",
|
||||
"crates/apps/game-realtime-webtransport-browser-smoke/src/lib.rs",
|
||||
"crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs",
|
||||
"crates/apps/game-reflex-poc-desktop/Cargo.toml",
|
||||
"crates/apps/game-snake-poc-desktop/Cargo.toml",
|
||||
"crates/apps/game-reflex-poc-tauri/Cargo.toml",
|
||||
@@ -55,6 +58,11 @@ REQUIRED_PATHS = (
|
||||
"crates/apps/game-snake-poc-tauri/frontend/ts/lifecycle.ts",
|
||||
"crates/apps/game-snake-poc-tauri/frontend/ts/main.ts",
|
||||
"crates/apps/game-snake-poc-tauri/frontend/ts/provenance.ts",
|
||||
"Web/game-realtime-webtransport-browser-smoke/package.json",
|
||||
"Web/game-realtime-webtransport-browser-smoke/tsconfig.json",
|
||||
"Web/game-realtime-webtransport-browser-smoke/vite.config.ts",
|
||||
"Web/game-realtime-webtransport-browser-smoke/frontend/main.html",
|
||||
"Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts",
|
||||
"Web/game-snake-poc/package.json",
|
||||
"Web/game-snake-poc/tsconfig.json",
|
||||
"Web/game-snake-poc/vite.config.ts",
|
||||
@@ -312,6 +320,39 @@ def audit_snake_tauri_generated_android(root: pathlib.Path) -> list[str]:
|
||||
|
||||
|
||||
|
||||
|
||||
def audit_webtransport_browser_smoke(root: pathlib.Path) -> list[str]:
|
||||
"""Validate the technical real-browser WebTransport smoke boundary."""
|
||||
|
||||
violations: list[str] = []
|
||||
package_path = root / "Web/game-realtime-webtransport-browser-smoke/package.json"
|
||||
vite_path = root / "Web/game-realtime-webtransport-browser-smoke/vite.config.ts"
|
||||
main_path = root / "Web/game-realtime-webtransport-browser-smoke/frontend/ts/main.ts"
|
||||
rust_manifest_path = root / "crates/apps/game-realtime-webtransport-browser-smoke/Cargo.toml"
|
||||
rust_main_path = root / "crates/apps/game-realtime-webtransport-browser-smoke/src/main.rs"
|
||||
if not all(path.exists() for path in (package_path, vite_path, main_path, rust_manifest_path, rust_main_path)):
|
||||
return violations
|
||||
package = package_path.read_text(encoding="utf-8")
|
||||
vite = vite_path.read_text(encoding="utf-8")
|
||||
main = main_path.read_text(encoding="utf-8")
|
||||
rust_manifest = rust_manifest_path.read_text(encoding="utf-8")
|
||||
rust_main = rust_main_path.read_text(encoding="utf-8")
|
||||
required_fragments = (
|
||||
(package, 'cargo build -p game-realtime-webtransport-browser-smoke --lib --target wasm32-unknown-unknown', "DIST-LAYOUT-077: browser WebTransport smoke must build its Rust WASM adapter"),
|
||||
(package, 'wasm-bindgen ../../builds/sasedev-games/', "DIST-LAYOUT-078: browser WebTransport smoke bindings must be generated outside the repository"),
|
||||
(vite, 'host: "127.0.0.1"', "DIST-LAYOUT-079: browser WebTransport smoke Vite host must stay on loopback"),
|
||||
(vite, 'port: 1435,', "DIST-LAYOUT-080: browser WebTransport smoke Vite port must stay deterministic"),
|
||||
(main, 'window.isSecureContext', "DIST-LAYOUT-081: browser WebTransport smoke must verify secure-context availability"),
|
||||
(main, '"WebTransport" in window', "DIST-LAYOUT-082: browser WebTransport smoke must verify WebTransport availability"),
|
||||
(main, 'run_browser_smoke(', "DIST-LAYOUT-083: browser WebTransport smoke must execute the Rust WASM path"),
|
||||
(rust_manifest, 'game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" }', "DIST-LAYOUT-084: browser WebTransport smoke adapter must consume the shared backend"),
|
||||
(rust_main, 'game-realtime-webtransport-browser-smoke: PASS', "DIST-LAYOUT-085: browser WebTransport smoke native peer must expose a deterministic PASS verdict"),
|
||||
)
|
||||
for content, fragment, message in required_fragments:
|
||||
if fragment not in content:
|
||||
violations.append(message)
|
||||
return violations
|
||||
|
||||
def audit_android_native_gradle(root: pathlib.Path) -> list[str]:
|
||||
"""Validate the minimum Gradle contract and multi-ABI native Rust consumers."""
|
||||
|
||||
@@ -402,6 +443,7 @@ def main() -> int:
|
||||
contract_violations.extend(audit_snake_tauri_android_integration(root))
|
||||
contract_violations.extend(audit_snake_tauri_generated_android(root))
|
||||
contract_violations.extend(audit_android_native_gradle(root))
|
||||
contract_violations.extend(audit_webtransport_browser_smoke(root))
|
||||
if missing:
|
||||
for relative in missing:
|
||||
print(f"DIST-LAYOUT-001: missing required path: {relative}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user