#![warn(missing_docs)]
use std::collections::HashMap;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::Mutex;
use bytes::Bytes;
use js_sys::Reflect;
use js_sys::Uint8Array;
use rings_core::dht::Did;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use wasm_bindgen_futures::spawn_local;
use wasm_bindgen_futures::JsFuture;
use web_sys::ReadableStream;
use web_sys::ReadableStreamDefaultReader;
use web_sys::WebTransport;
use web_sys::WritableStream;
use web_sys::WritableStreamDefaultWriter;
use crate::error::Error;
use crate::error::Result;
use crate::extension::ext::Scope;
use crate::extension::protocols::relay::RelayCommand;
use crate::extension::transport::Frame;
use crate::extension::transport::Initiator;
use crate::extension::transport::SessionKey;
use crate::extension::transport::TransportKind;
enum Outbound {
Data(Bytes),
Shutdown,
}
enum SessionHandle {
Opening {
queue: Vec<Outbound>,
generation: u64,
},
Ready {
writer: WritableStreamDefaultWriter,
transport: WebTransport,
generation: u64,
},
}
impl SessionHandle {
fn generation(&self) -> u64 {
match self {
SessionHandle::Opening { generation, .. } => *generation,
SessionHandle::Ready { generation, .. } => *generation,
}
}
}
#[derive(Default)]
pub(crate) struct WtSessions {
map: Mutex<HashMap<SessionKey, SessionHandle>>,
generations: AtomicU64,
}
impl WtSessions {
pub fn new() -> Self {
Self::default()
}
pub async fn connect(
self: Arc<Self>,
scope: Scope,
key: SessionKey,
url: String,
kind: TransportKind,
) {
debug_assert_eq!(
scope.namespace(),
key.namespace.as_str(),
"relay engine acted with a scope outside the session's namespace"
);
let generation = self.open_slot(key.clone());
match open(url.as_str(), kind).await {
Ok((transport, readable, writer)) => {
if self.promote(&key, generation, writer, transport.clone()) {
self.spawn_read_loop(scope, key, readable, generation);
} else {
transport.close();
}
}
Err(e) => {
tracing::error!("WebTransport connect to {url} failed: {e:?}");
if self.close_if_current(&scope, &key, generation).await {
let _ = send_frame(&scope, key.peer, Frame::Close {
session: key.session,
from_opener: matches!(key.initiator, Initiator::Local),
})
.await;
}
}
}
}
pub async fn write(&self, key: &SessionKey, bytes: Bytes) {
let Some(writer) = self.ready_writer_or_queue(key, Outbound::Data(bytes.clone())) else {
return;
};
let chunk = Uint8Array::from(bytes.as_ref());
let _ = JsFuture::from(writer.write_with_chunk(chunk.as_ref())).await;
}
pub async fn shutdown(&self, key: &SessionKey) {
if let Some(writer) = self.ready_writer_or_queue(key, Outbound::Shutdown) {
let _ = JsFuture::from(writer.close()).await;
}
}
pub async fn close(&self, scope: &Scope, key: &SessionKey) {
let removed = self.map.lock().ok().and_then(|mut map| map.remove(key));
self.finish_close(scope, key, removed).await;
}
async fn close_if_current(&self, scope: &Scope, key: &SessionKey, generation: u64) -> bool {
let removed = self.map.lock().ok().and_then(|mut map| {
let current = map.get(key).map(|handle| handle.generation());
(current == Some(generation))
.then(|| map.remove(key))
.flatten()
});
self.finish_close(scope, key, removed).await
}
async fn finish_close(
&self,
scope: &Scope,
key: &SessionKey,
removed: Option<SessionHandle>,
) -> bool {
let Some(handle) = removed else {
return false;
};
if let SessionHandle::Ready { transport, .. } = handle {
transport.close();
}
inject_untrack(scope, key).await;
true
}
fn open_slot(&self, key: SessionKey) -> u64 {
let generation = self.generations.fetch_add(1, Ordering::Relaxed);
self.insert(key, SessionHandle::Opening {
queue: Vec::new(),
generation,
});
generation
}
fn promote(
&self,
key: &SessionKey,
generation: u64,
writer: WritableStreamDefaultWriter,
transport: WebTransport,
) -> bool {
let Ok(mut map) = self.map.lock() else {
return false;
};
let queue = match map.get(key) {
Some(SessionHandle::Opening {
generation: current,
..
}) if *current == generation => match map.remove(key) {
Some(SessionHandle::Opening { queue, .. }) => queue,
_ => return false,
},
_ => return false,
};
for op in queue {
match op {
Outbound::Data(bytes) => {
let chunk = Uint8Array::from(bytes.as_ref());
let _ = writer.write_with_chunk(chunk.as_ref());
}
Outbound::Shutdown => {
let _ = writer.close();
}
}
}
map.insert(key.clone(), SessionHandle::Ready {
writer,
transport,
generation,
});
true
}
fn ready_writer_or_queue(
&self,
key: &SessionKey,
op: Outbound,
) -> Option<WritableStreamDefaultWriter> {
let mut map = self.map.lock().ok()?;
match map.get_mut(key)? {
SessionHandle::Ready { writer, .. } => Some(writer.clone()),
SessionHandle::Opening { queue, .. } => {
queue.push(op);
None
}
}
}
fn insert(&self, key: SessionKey, handle: SessionHandle) {
if let Ok(mut map) = self.map.lock() {
if let Some(SessionHandle::Ready { transport, .. }) = map.insert(key, handle) {
transport.close();
}
}
}
fn spawn_read_loop(
self: &Arc<Self>,
scope: Scope,
key: SessionKey,
readable: ReadableStream,
generation: u64,
) {
let sessions = self.clone();
spawn_local(async move {
let peer = key.peer;
let session = key.session;
let from_opener = matches!(key.initiator, Initiator::Local);
let reader: ReadableStreamDefaultReader = match readable.get_reader().dyn_into() {
Ok(reader) => reader,
Err(_) => return,
};
loop {
let result = match JsFuture::from(reader.read()).await {
Ok(result) => result,
Err(_) => break,
};
let done = Reflect::get(&result, &JsValue::from_str("done"))
.ok()
.and_then(|v| v.as_bool())
.unwrap_or(true);
if done {
break;
}
let value = match Reflect::get(&result, &JsValue::from_str("value")) {
Ok(value) => value,
Err(_) => break,
};
let bytes = Bytes::from(Uint8Array::new(&value).to_vec());
if send_frame(&scope, peer, Frame::Data {
session,
from_opener,
bytes,
})
.await
.is_err()
{
break;
}
}
if sessions.close_if_current(&scope, &key, generation).await {
let _ = send_frame(&scope, peer, Frame::Close {
session,
from_opener,
})
.await;
}
});
}
}
async fn open(
url: &str,
kind: TransportKind,
) -> std::result::Result<(WebTransport, ReadableStream, WritableStreamDefaultWriter), JsValue> {
let transport = WebTransport::new(url)?;
JsFuture::from(transport.ready()).await?;
let (readable, writable): (ReadableStream, WritableStream) = match kind {
TransportKind::Tcp => {
let bidi = JsFuture::from(transport.create_bidirectional_stream()).await?;
let bidi: web_sys::WebTransportBidirectionalStream = bidi.unchecked_into();
(
bidi.readable().unchecked_into(),
bidi.writable().unchecked_into(),
)
}
TransportKind::Udp => {
let datagrams = transport.datagrams();
(datagrams.readable(), datagrams.writable())
}
};
let writer = writable.get_writer()?;
Ok((transport, readable, writer))
}
async fn send_frame(scope: &Scope, peer: Did, frame: Frame) -> Result<()> {
let payload = bincode::serialize(&frame).map_err(|_| Error::EncodeError)?;
scope.send(peer, Bytes::from(payload)).await
}
async fn inject_untrack(scope: &Scope, key: &SessionKey) {
let command = RelayCommand::<String>::Untrack {
peer: key.peer,
session: key.session,
initiator: key.initiator,
};
if let Ok(bytes) = bincode::serialize(&command) {
if let Err(e) = scope.inject(Bytes::from(bytes)).await {
tracing::warn!(
"relay Untrack inject failed for {key:?}: {e:?}; pure state may still list \
this (now dropped) session"
);
}
}
}