use std::{
cell::RefCell,
collections::VecDeque,
future::poll_fn,
rc::{Rc, Weak},
task::{Poll, Waker},
};
use crate::client::web::clock;
use crate::live::{BadFrame, Frame, PING, Post, Refused, Retry, Session, Subscription, Tick};
use js_sys::{Array, Function, Math, Reflect};
use wasm_bindgen::{JsCast, JsValue, closure::Closure};
use web_sys::{CloseEvent, MessageEvent, WebSocket};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Event {
Opened,
Frame(Box<Frame>),
Unreadable(BadFrame),
Closed { reason: String, retry: Retry },
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum FeedError {
#[error(transparent)]
Refused(#[from] Refused),
#[error("the feed has stopped")]
Stopped,
}
pub struct Feed {
driver: Rc<RefCell<Driver>>,
}
impl Feed {
#[must_use]
pub fn start(session: Session) -> Feed {
let url = session.url();
Feed::start_at(url, session)
}
#[must_use]
pub fn start_at(url: impl Into<String>, session: Session) -> Feed {
let global: JsValue = js_sys::global().into();
let timers = function(&global, "setTimeout").zip(function(&global, "clearTimeout"));
let driver = Rc::new_cyclic(|driver| {
RefCell::new(Driver {
url: url.into(),
session,
global,
timers,
socket: None,
timer: None,
calls: Calls::new(driver),
events: VecDeque::new(),
waker: None,
stopped: false,
})
});
if let Ok(mut driver) = driver.try_borrow_mut() {
if driver.timers.is_some() {
driver.dial();
} else {
driver.stop();
}
}
Feed { driver }
}
pub async fn subscribe(&self, subscription: Subscription) -> Result<(), FeedError> {
self.with(|driver| {
if let Some(frame) = driver.session.subscribe(subscription)? {
driver.carry(&frame);
}
Ok(())
})
}
pub fn unsubscribe(&self, subscription: Subscription) -> Result<(), FeedError> {
self.with(|driver| {
if let Some(frame) = driver.session.unsubscribe(&subscription) {
driver.carry(&frame);
}
Ok(())
})
}
pub async fn post(&self, request: Post) -> Result<u64, FeedError> {
self.with(|driver| {
let (id, frame) = driver.session.post(&request)?;
driver.carry(&frame);
Ok(id)
})
}
pub async fn next(&mut self) -> Option<Event> {
poll_fn(|cx| {
let Ok(mut driver) = self.driver.try_borrow_mut() else {
cx.waker().wake_by_ref();
return Poll::Pending;
};
if let Some(event) = driver.events.pop_front() {
return Poll::Ready(Some(event));
}
if driver.stopped {
return Poll::Ready(None);
}
driver.waker = Some(cx.waker().clone());
Poll::Pending
})
.await
}
pub fn close(self) {
drop(self);
}
fn with<T>(&self, act: impl FnOnce(&mut Driver) -> Result<T, FeedError>) -> Result<T, FeedError> {
let mut driver = self.driver.try_borrow_mut().map_err(|_| FeedError::Stopped)?;
if driver.stopped {
return Err(FeedError::Stopped);
}
act(&mut driver)
}
}
impl Drop for Feed {
fn drop(&mut self) {
if let Ok(mut driver) = self.driver.try_borrow_mut() {
driver.stop();
}
}
}
struct Driver {
url: String,
session: Session,
global: JsValue,
timers: Option<(Function, Function)>,
socket: Option<WebSocket>,
timer: Option<JsValue>,
calls: Calls,
events: VecDeque<Event>,
waker: Option<Waker>,
stopped: bool,
}
impl Driver {
fn dial(&mut self) {
match connect(&self.global, &self.url) {
Ok(socket) => {
socket.set_onopen(Some(self.calls.open.as_ref().unchecked_ref()));
socket.set_onmessage(Some(self.calls.message.as_ref().unchecked_ref()));
socket.set_onclose(Some(self.calls.close.as_ref().unchecked_ref()));
socket.set_onerror(Some(self.calls.error.as_ref().unchecked_ref()));
self.socket = Some(socket);
}
Err(reason) => self.lost(reason),
}
}
fn opened(&mut self) {
for frame in self.session.opened(clock()) {
if let Err(reason) = self.send(&frame) {
return self.lost(reason);
}
}
self.events.push_back(Event::Opened);
self.arm_tick();
}
fn received(&mut self, message: &MessageEvent) {
let Some(text) = message.data().as_string() else { return };
let event = match self.session.received(clock(), &text) {
Ok(frame) => Event::Frame(Box::new(frame)),
Err(bad) => Event::Unreadable(bad),
};
self.events.push_back(event);
}
fn woke(&mut self) {
self.timer = None;
if self.session.is_open() {
match self.session.tick(clock()) {
Tick::Wait => {}
Tick::Ping => {
if let Err(reason) = self.send(PING) {
return self.lost(reason);
}
}
Tick::Reconnect => return self.lost("nothing arrived for too long".to_owned()),
}
self.arm_tick();
} else if self.socket.is_none() {
self.dial();
}
}
fn carry(&mut self, frame: &str) {
if let Err(reason) = self.send(frame) {
self.lost(reason);
}
}
fn send(&self, frame: &str) -> Result<(), String> {
let Some(socket) = &self.socket else { return Err("no socket to send on".to_owned()) };
socket.send_with_str(frame).map_err(|error| said(&error).unwrap_or_else(|| "the send failed".to_owned()))
}
fn lost(&mut self, reason: String) {
self.hang_up();
let retry = self.session.closed(clock(), random());
let after = retry.after_ms;
self.events.push_back(Event::Closed { reason, retry });
self.arm(after);
}
fn hang_up(&mut self) {
if let Some(socket) = self.socket.take() {
socket.set_onopen(None);
socket.set_onmessage(None);
socket.set_onclose(None);
socket.set_onerror(None);
let _ = socket.close();
}
}
fn stop(&mut self) {
self.hang_up();
self.disarm();
self.stopped = true;
}
fn arm_tick(&mut self) {
if let Some(at) = self.session.due() {
self.arm(at.saturating_sub(clock()));
}
}
fn arm(&mut self, ms: u64) {
self.disarm();
let Some((set_timeout, _)) = &self.timers else { return self.stop() };
let delay = JsValue::from(u32::try_from(ms).map_or(LONGEST_DELAY, |ms| ms.min(LONGEST_DELAY)));
match set_timeout.call2(&self.global, self.calls.wake.as_ref(), &delay) {
Ok(timer) => self.timer = Some(timer),
Err(_) => self.stop(),
}
}
fn disarm(&mut self) {
if let (Some(timer), Some((_, clear_timeout))) = (self.timer.take(), &self.timers) {
let _ = clear_timeout.call1(&self.global, &timer);
}
}
fn wakeable(&mut self) -> Option<Waker> {
if self.events.is_empty() && !self.stopped { None } else { self.waker.take() }
}
}
const LONGEST_DELAY: u32 = 0x7fff_ffff;
struct Calls {
open: Closure<dyn FnMut()>,
message: Closure<dyn FnMut(MessageEvent)>,
close: Closure<dyn FnMut(CloseEvent)>,
error: Closure<dyn FnMut(JsValue)>,
wake: Closure<dyn FnMut()>,
}
impl Calls {
fn new(driver: &Weak<RefCell<Driver>>) -> Calls {
let (open, message, close, error, wake) =
(driver.clone(), driver.clone(), driver.clone(), driver.clone(), driver.clone());
Calls {
open: Closure::new(move || on(&open, Driver::opened)),
message: Closure::new(move |event: MessageEvent| on(&message, |driver| driver.received(&event))),
close: Closure::new(move |event: CloseEvent| {
let reason = match event.reason() {
reason if reason.is_empty() => format!("the socket closed ({})", event.code()),
reason => reason,
};
on(&close, |driver| driver.lost(reason));
}),
error: Closure::new(move |event: JsValue| {
let reason = said(&event).unwrap_or_else(|| "the socket failed".to_owned());
on(&error, |driver| driver.lost(reason));
}),
wake: Closure::new(move || on(&wake, Driver::woke)),
}
}
}
fn on(driver: &Weak<RefCell<Driver>>, act: impl FnOnce(&mut Driver)) {
let Some(driver) = driver.upgrade() else { return };
let waker = {
let Ok(mut driver) = driver.try_borrow_mut() else { return };
act(&mut driver);
driver.wakeable()
};
if let Some(waker) = waker {
waker.wake();
}
}
fn connect(global: &JsValue, url: &str) -> Result<WebSocket, String> {
let class = function(global, "WebSocket").ok_or_else(|| "this runtime has no WebSocket".to_owned())?;
let socket = Reflect::construct(&class, &Array::of1(&JsValue::from_str(url)))
.map_err(|error| said(&error).unwrap_or_else(|| "the socket could not be made".to_owned()))?;
Ok(socket.unchecked_into())
}
fn function(global: &JsValue, name: &str) -> Option<Function> {
Reflect::get(global, &JsValue::from_str(name)).ok()?.dyn_into().ok()
}
fn said(error: &JsValue) -> Option<String> {
let message = Reflect::get(error, &JsValue::from_str("message")).ok()?.as_string()?;
(!message.is_empty()).then_some(message)
}
fn random() -> u64 {
let bits = Math::random().to_bits().wrapping_mul(0x9e37_79b9_7f4a_7c15);
bits ^ (bits >> 29) ^ clock().rotate_left(29)
}
#[cfg(test)]
#[cfg(target_arch = "wasm32")]
#[path = "tests/socket.rs"]
mod tests;