use std::{
cell::RefCell,
cmp,
future::Future,
io, mem,
pin::Pin,
rc::Rc,
task::{Context, Poll, Waker},
time::Duration,
};
use js_sys::Promise;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{JsFuture, future_to_promise};
use crate::{BatchError, Channel, Receiver, Sender};
pub fn spawn<T: Channel + 'static, F: Future<Output = Result<(), BatchError<T>>> + 'static>(
receiver: Receiver<T>,
on_batch: impl FnMut(T) -> F + 'static,
) -> io::Result<SpawnHandle> {
let promise = future_to_promise(async move {
receiver.exec(|delay| Park::new(delay), on_batch).await;
Ok(JsValue::UNDEFINED)
});
Ok(SpawnHandle { promise })
}
pub struct SpawnHandle {
promise: Promise,
}
impl SpawnHandle {
pub fn into_promise(self) -> Promise {
self.promise
}
pub async fn join(self) {
let _ = JsFuture::from(self.promise).await;
}
}
pub async fn send<T: Channel>(
sender: &Sender<T>,
msg: T::Item,
timeout: Duration,
) -> Result<(), BatchError<T::Item>> {
let start = performance_now();
sender
.send_or_wait(
msg,
timeout,
|| performance_now().saturating_sub(start),
|sender, timeout| async move {
let (notifier, notified) = futures::channel::oneshot::channel();
sender.when_empty(move || {
let _ = notifier.send(true);
});
wait(notified, timeout).await;
},
)
.await
}
pub async fn flush<T: Channel>(sender: &Sender<T>, timeout: Duration) -> bool {
let (notifier, notified) = futures::channel::oneshot::channel();
sender.when_flushed_inner(move |flushed| {
let _ = notifier.send(flushed);
});
wait(notified, timeout).await
}
async fn wait(mut notified: futures::channel::oneshot::Receiver<bool>, timeout: Duration) -> bool {
if let Ok(Some(value)) = notified.try_recv() {
return value;
}
if timeout == Duration::ZERO {
return false;
}
let timeout = Park::new(timeout);
match futures::future::select(notified, timeout).await {
futures::future::Either::Left((Ok(value), _)) => value,
futures::future::Either::Left((Err(_), _)) => false,
futures::future::Either::Right(((), _)) => false,
}
}
struct Park {
delay: Option<Duration>,
timeout: Option<Timeout>,
state: Rc<RefCell<ParkState>>,
}
impl Drop for Park {
fn drop(&mut self) {
ParkState::wake(&self.state);
}
}
impl Park {
fn new(delay: Duration) -> Self {
Park {
delay: Some(delay),
timeout: None,
state: Rc::new(RefCell::new(ParkState {
done: false,
wakers: Vec::new(),
})),
}
}
}
impl ParkState {
fn wake(state: &Rc<RefCell<Self>>) {
let mut state = state.borrow_mut();
state.done = true;
let wakers = mem::take(&mut state.wakers);
drop(state);
for waker in wakers {
waker.wake();
}
}
}
struct Timeout {
_closure: Closure<dyn Fn()>,
token: f64,
}
impl Drop for Timeout {
fn drop(&mut self) {
clear_timeout(self.token);
}
}
struct ParkState {
done: bool,
wakers: Vec<Waker>,
}
impl Future for Park {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
let unpinned = unsafe { self.get_unchecked_mut() };
if unpinned.state.borrow_mut().done {
return Poll::Ready(());
}
let mut state = unpinned.state.borrow_mut();
let waker = cx.waker();
if !state.wakers.iter().any(|w| w.will_wake(waker)) {
state.wakers.push(waker.clone());
}
drop(state);
if let Some(delay) = unpinned.delay.take() {
let state = unpinned.state.clone();
let closure = Closure::<dyn Fn()>::new(move || {
ParkState::wake(&state);
});
let token = set_timeout(&closure, cmp::max(1, delay.as_millis() as u32));
unpinned.timeout = Some(Timeout {
token,
_closure: closure,
});
}
Poll::Pending
}
}
fn performance_now() -> Duration {
let origin_millis = PERFORMANCE.with(|performance| performance.time_origin());
let now_millis = now();
let origin_nanos = (origin_millis * 1_000_000.0) as u128;
let now_nanos = (now_millis * 1_000_000.0) as u128;
let timestamp_nanos = origin_nanos + now_nanos;
let timestamp_secs = (timestamp_nanos / 1_000_000_000) as u64;
let timestamp_subsec_nanos = (timestamp_nanos % 1_000_000_000) as u32;
Duration::new(timestamp_secs, timestamp_subsec_nanos)
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = "setTimeout")]
fn set_timeout(closure: &Closure<dyn Fn()>, millis: u32) -> f64;
#[wasm_bindgen(js_name = "clearTimeout")]
fn clear_timeout(token: f64);
}
#[wasm_bindgen]
extern "C" {
type Performance;
#[wasm_bindgen(thread_local_v2, js_name = performance)]
static PERFORMANCE: Performance;
#[wasm_bindgen(method, getter = timeOrigin)]
fn time_origin(this: &Performance) -> f64;
#[wasm_bindgen(js_namespace = performance)]
fn now() -> f64;
}
#[cfg(all(
test,
target_arch = "wasm32",
target_vendor = "unknown",
target_os = "unknown"
))]
mod tests {
use super::*;
use futures::channel::oneshot;
use std::sync::{Arc, Mutex};
use wasm_bindgen_test::*;
#[wasm_bindgen_test]
async fn promise_resolves_on_sender_drop() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(1024);
let handle = spawn(receiver, |batch| async move {
let _ = batch;
Ok(())
})
.unwrap();
drop(sender);
handle.join().await;
}
#[wasm_bindgen_test]
async fn spawn_processes_batches() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(1024);
let count = Arc::new(Mutex::new(0));
let handle = spawn(receiver, {
let count = count.clone();
move |batch| {
let count = count.clone();
async move {
*count.lock().unwrap() = batch.len();
Ok(())
}
}
})
.unwrap();
for i in 0..100 {
sender.send(i);
}
drop(sender);
handle.join().await;
assert_eq!(100, *count.lock().unwrap());
}
#[wasm_bindgen_test]
async fn send_waits_for_processing() {
let (sender, receiver) = crate::bounded::<Vec<()>>(2);
let total = Arc::new(Mutex::new(0));
let handle = spawn(receiver, {
let total = total.clone();
move |batch| {
let total = total.clone();
async move {
*total.lock().unwrap() += batch.len();
Ok(())
}
}
})
.unwrap();
send(&sender, (), Duration::from_secs(1)).await.unwrap();
send(&sender, (), Duration::from_secs(1)).await.unwrap();
send(&sender, (), Duration::from_secs(1)).await.unwrap();
assert_eq!(2, *total.lock().unwrap());
drop(sender);
handle.join().await;
}
#[wasm_bindgen_test]
async fn flush_waits_for_completion() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(1024);
let total = Arc::new(Mutex::new(0));
let handle = spawn(receiver, {
let total = total.clone();
move |batch| {
let total = total.clone();
async move {
*total.lock().unwrap() += batch.len();
Ok(())
}
}
})
.unwrap();
for i in 0..100 {
sender.send(i);
}
assert_eq!(0, *total.lock().unwrap());
let flushed = flush(&sender, Duration::from_millis(10)).await;
assert!(flushed);
assert_eq!(100, *total.lock().unwrap());
drop(sender);
handle.join().await;
}
#[wasm_bindgen_test]
async fn flush_times_out() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(1024);
let total = Arc::new(Mutex::new(0));
let handle = spawn(receiver, {
let total = total.clone();
move |batch| {
let total = total.clone();
async move {
Park::new(Duration::from_millis(50)).await;
*total.lock().unwrap() += batch.len();
Ok(())
}
}
})
.unwrap();
for i in 0..100 {
sender.send(i);
}
assert_eq!(0, *total.lock().unwrap());
let flushed = flush(&sender, Duration::from_millis(1)).await;
assert!(!flushed);
drop(sender);
handle.join().await;
}
#[wasm_bindgen_test]
async fn failing_receiver_does_not_cause_havoc() {
let (sender, receiver) = crate::bounded::<Vec<()>>(1024);
let handle = spawn(receiver, |_| async move {
Err(BatchError::no_retry(std::io::Error::new(
std::io::ErrorKind::Other,
"explicit failure",
)))
})
.unwrap();
sender.send(());
drop(sender);
handle.join().await;
}
#[wasm_bindgen_test]
fn try_send_on_closed_channel() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(10);
drop(receiver);
let result = sender.try_send(1);
assert!(result.is_err());
let err = result.err().unwrap();
assert!(err.into_retryable().is_none());
}
#[wasm_bindgen_test]
async fn truncation_keeps_most_recent() {
let (sender, receiver) = crate::bounded::<Vec<i32>>(5);
let received = Arc::new(Mutex::new(Vec::new()));
let handle = spawn(receiver, {
let received = received.clone();
move |batch| {
let received = received.clone();
async move {
received.lock().unwrap().extend(batch);
Ok(())
}
}
})
.unwrap();
for i in 0..10 {
sender.send(i);
}
drop(sender);
handle.join().await;
assert_eq!(vec![5, 6, 7, 8, 9], *received.lock().unwrap());
}
#[wasm_bindgen_test]
async fn park_completes_on_timeout() {
let start = js_sys::Date::new_0().get_time();
let delay_ms = 17.0;
Park::new(Duration::from_millis(delay_ms as u64)).await;
let elapsed = js_sys::Date::new_0().get_time() - start;
assert!(
elapsed >= delay_ms / 10.0,
"Expected at least 10ms, got {}",
elapsed
);
assert!(
elapsed <= delay_ms * 10.0,
"Expected less than 100ms, got {} - future may have hung",
elapsed
);
}
#[wasm_bindgen_test]
async fn park_does_not_hang_when_dropped_early() {
let (tx, rx) = oneshot::channel::<()>();
let _ = tx.send(());
let result = futures::future::select(rx, Park::new(Duration::from_millis(100))).await;
assert!(matches!(result, futures::future::Either::Left((Ok(()), _))));
}
#[wasm_bindgen_test]
async fn park_zero_duration() {
Park::new(Duration::ZERO).await;
}
}