0.3.5-alpha.7
This commit is contained in:
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user