#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Condvar, Mutex};
use std::{
cell::RefCell,
future::Future,
pin::Pin,
rc::Rc,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
},
task::{Context, Poll, Waker},
time::Duration,
};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::JsCast;
use web_time::Instant;
use crate::{
hooks::{mutableStateOf, remember},
runtime::{RuntimeHandle, TaskHandle, current_runtime_handle},
state::{MutableState, State},
};
pub fn spawn_ui_task(future: impl Future<Output = ()> + 'static) -> Option<TaskHandle> {
current_runtime_handle().and_then(|runtime| runtime.spawn_ui(future))
}
#[derive(Clone)]
pub struct CoroutineScope {
inner: Rc<ScopeInner>,
}
struct ScopeInner {
runtime: Option<RuntimeHandle>,
tasks: RefCell<Vec<TaskHandle>>,
}
impl Drop for ScopeInner {
fn drop(&mut self) {
for task in self.tasks.borrow_mut().drain(..) {
task.cancel();
}
}
}
impl CoroutineScope {
pub fn launch(&self, future: impl Future<Output = ()> + 'static) {
let Some(runtime) = self.inner.runtime.clone() else {
log::warn!("cranpose: a coroutine scope with no runtime dropped its work");
return;
};
self.inner
.tasks
.borrow_mut()
.retain(|task| !task.is_finished());
if let Some(handle) = runtime.spawn_ui(future) {
self.inner.tasks.borrow_mut().push(handle);
}
}
pub fn cancel(&self) {
for task in self.inner.tasks.borrow_mut().drain(..) {
task.cancel();
}
}
#[cfg(test)]
pub(crate) fn probe_identity(&self) -> usize {
Rc::as_ptr(&self.inner) as *const () as usize
}
}
#[allow(non_snake_case)]
#[track_caller]
pub fn rememberCoroutineScope() -> CoroutineScope {
remember(|| CoroutineScope {
inner: Rc::new(ScopeInner {
runtime: current_runtime_handle(),
tasks: RefCell::new(Vec::new()),
}),
})
.with(|scope| scope.clone())
}
pub fn delay(duration: Duration) -> Delay {
Delay {
deadline: Instant::now() + duration,
armed: false,
fired: Arc::new(AtomicBool::new(false)),
}
}
pub struct Delay {
deadline: Instant,
armed: bool,
fired: Arc<AtomicBool>,
}
impl Future for Delay {
type Output = ();
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
if self.fired.load(Ordering::Acquire) || Instant::now() >= self.deadline {
return Poll::Ready(());
}
let this = self.get_mut();
if !this.armed {
this.armed = true;
timer().arm(
this.deadline,
context.waker().clone(),
Arc::clone(&this.fired),
);
}
Poll::Pending
}
}
pub async fn interval(period: Duration, mut tick: impl FnMut()) {
loop {
delay(period).await;
tick();
}
}
#[cfg(not(target_arch = "wasm32"))]
struct Alarm {
deadline: Instant,
waker: Waker,
fired: Arc<AtomicBool>,
}
struct Timer {
#[cfg(not(target_arch = "wasm32"))]
alarms: Mutex<Vec<Alarm>>,
#[cfg(not(target_arch = "wasm32"))]
wake: Condvar,
}
fn timer() -> &'static Timer {
static TIMER: OnceLock<&'static Timer> = OnceLock::new();
TIMER.get_or_init(|| {
let timer: &'static Timer = Box::leak(Box::new(Timer::new()));
timer.start();
timer
})
}
#[cfg(not(target_arch = "wasm32"))]
impl Timer {
fn new() -> Self {
Self {
alarms: Mutex::new(Vec::new()),
wake: Condvar::new(),
}
}
fn start(&'static self) {
std::thread::Builder::new()
.name("cranpose-timer".to_string())
.spawn(move || self.run())
.expect("the timer thread starts");
}
fn run(&self) {
let mut alarms = self
.alarms
.lock()
.unwrap_or_else(|error| error.into_inner());
loop {
let now = Instant::now();
let mut due = Vec::new();
let mut next: Option<Duration> = None;
alarms.retain(|alarm| {
if alarm.deadline <= now {
due.push((alarm.waker.clone(), Arc::clone(&alarm.fired)));
false
} else {
let remaining = alarm.deadline - now;
next = Some(next.map_or(remaining, |current| current.min(remaining)));
true
}
});
if !due.is_empty() {
drop(alarms);
for (waker, fired) in due {
fired.store(true, Ordering::Release);
waker.wake();
}
alarms = self
.alarms
.lock()
.unwrap_or_else(|error| error.into_inner());
continue;
}
alarms = match next {
Some(timeout) => {
self.wake
.wait_timeout(alarms, timeout)
.unwrap_or_else(|error| error.into_inner())
.0
}
None => self
.wake
.wait(alarms)
.unwrap_or_else(|error| error.into_inner()),
};
}
}
fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
let mut alarms = self
.alarms
.lock()
.unwrap_or_else(|error| error.into_inner());
alarms.push(Alarm {
deadline,
waker,
fired,
});
self.wake.notify_one();
}
}
#[cfg(target_arch = "wasm32")]
impl Timer {
fn new() -> Self {
Self {}
}
fn start(&'static self) {}
fn arm(&self, deadline: Instant, waker: Waker, fired: Arc<AtomicBool>) {
let millis = deadline
.saturating_duration_since(Instant::now())
.as_millis()
.min(i32::MAX as u128) as i32;
let callback = wasm_bindgen::closure::Closure::once_into_js(move || {
fired.store(true, Ordering::Release);
waker.wake();
});
let scheduled = web_sys::window().and_then(|window| {
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
callback.unchecked_ref(),
millis,
)
.ok()
});
if scheduled.is_none() {
log::warn!("cranpose: no window timer is available; the delay resolves immediately");
}
}
}
pub struct EventChannel<T: 'static> {
shared: Rc<ChannelShared<T>>,
}
struct ChannelShared<T: 'static> {
ready: RefCell<std::collections::VecDeque<T>>,
closed: std::cell::Cell<bool>,
delivered: std::cell::Cell<usize>,
wakers: RefCell<Vec<Waker>>,
}
impl<T: 'static> ChannelShared<T> {
fn wake_all(&self) {
for waker in self.wakers.borrow_mut().drain(..) {
waker.wake();
}
}
}
impl<T: 'static> Default for EventChannel<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: 'static> EventChannel<T> {
pub fn new() -> Self {
Self {
shared: Rc::new(ChannelShared {
ready: RefCell::new(std::collections::VecDeque::new()),
closed: std::cell::Cell::new(false),
delivered: std::cell::Cell::new(0),
wakers: RefCell::new(Vec::new()),
}),
}
}
pub fn stream(&self) -> EventStream<T> {
EventStream {
shared: Rc::clone(&self.shared),
}
}
pub fn send(&self, event: T) {
if self.shared.closed.get() {
return;
}
self.shared.ready.borrow_mut().push_back(event);
self.shared.wake_all();
}
pub fn close(&self) {
if self.shared.closed.get() {
return;
}
self.shared.closed.set(true);
self.shared.wake_all();
}
pub fn is_closed(&self) -> bool {
self.shared.closed.get()
}
pub fn pending(&self) -> usize {
self.shared.ready.borrow().len()
}
}
pub struct EventStream<T: 'static> {
shared: Rc<ChannelShared<T>>,
}
impl<T: 'static> Clone for EventStream<T> {
fn clone(&self) -> Self {
Self {
shared: Rc::clone(&self.shared),
}
}
}
impl<T: 'static> EventStream<T> {
pub fn next(&self) -> EventStreamNext<T> {
EventStreamNext {
shared: Rc::clone(&self.shared),
}
}
pub fn delivered(&self) -> usize {
self.shared.delivered.get()
}
}
pub struct EventStreamNext<T: 'static> {
shared: Rc<ChannelShared<T>>,
}
impl<T: 'static> Future for EventStreamNext<T> {
type Output = Option<T>;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Option<T>> {
if let Some(event) = self.shared.ready.borrow_mut().pop_front() {
self.shared.delivered.set(self.shared.delivered.get() + 1);
return Poll::Ready(Some(event));
}
if self.shared.closed.get() {
return Poll::Ready(None);
}
self.shared
.wakers
.borrow_mut()
.push(context.waker().clone());
Poll::Pending
}
}
#[allow(non_snake_case)]
#[track_caller]
pub fn CollectEvents<T, K>(stream: EventStream<T>, key: K, on_event: impl FnMut(T) + 'static)
where
T: 'static,
K: PartialEq + 'static,
{
crate::__launched_effect_async_impl(
crate::caller_location_key(),
std::panic::Location::caller().into(),
key,
move |_scope| {
let mut on_event = on_event;
Box::pin(async move {
while let Some(event) = stream.next().await {
on_event(event);
}
})
},
);
}
#[allow(non_snake_case)]
#[track_caller]
pub fn collectAsState<T, K>(stream: EventStream<T>, key: K, initial: T) -> State<T>
where
T: Clone + 'static,
K: PartialEq + 'static,
{
let state = remember(|| mutableStateOf(initial)).with(|state| *state);
let sink = state;
CollectEvents(stream, key, move |event| sink.set(event));
state.as_state()
}
pub struct EventSender<T: Send + 'static> {
#[cfg(not(target_arch = "wasm32"))]
dispatcher: crate::runtime::UiDispatcher,
bridge: u64,
_events: std::marker::PhantomData<fn(T)>,
}
impl<T: Send + 'static> Clone for EventSender<T> {
fn clone(&self) -> Self {
Self {
#[cfg(not(target_arch = "wasm32"))]
dispatcher: self.dispatcher.clone(),
bridge: self.bridge,
_events: std::marker::PhantomData,
}
}
}
impl<T: Send + 'static> EventSender<T> {
pub fn send(&self, event: T) {
let bridge = self.bridge;
#[cfg(not(target_arch = "wasm32"))]
self.dispatcher
.post(move || deliver_bridged::<T>(bridge, event));
#[cfg(target_arch = "wasm32")]
deliver_bridged::<T>(bridge, event);
}
}
thread_local! {
static BRIDGES: RefCell<std::collections::HashMap<u64, Rc<dyn std::any::Any>>> =
RefCell::new(std::collections::HashMap::new());
}
static NEXT_BRIDGE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
fn deliver_bridged<T: Send + 'static>(bridge: u64, event: T) {
let channel = BRIDGES.with(|bridges| bridges.borrow().get(&bridge).cloned());
let Some(channel) = channel else {
log::debug!("event bridge {bridge} is gone, one event dropped");
return;
};
if let Ok(channel) = channel.downcast::<EventChannel<T>>() {
channel.send(event);
}
}
struct Bridge<T: Send + 'static> {
id: u64,
channel: Rc<EventChannel<T>>,
}
impl<T: Send + 'static> Bridge<T> {
fn new() -> Self {
let id = NEXT_BRIDGE.fetch_add(1, Ordering::Relaxed);
let channel = Rc::new(EventChannel::<T>::new());
BRIDGES.with(|bridges| {
bridges
.borrow_mut()
.insert(id, Rc::clone(&channel) as Rc<dyn std::any::Any>)
});
Self { id, channel }
}
}
impl<T: Send + 'static> Drop for Bridge<T> {
fn drop(&mut self) {
BRIDGES.with(|bridges| bridges.borrow_mut().remove(&self.id));
self.channel.close();
}
}
#[allow(non_snake_case)]
#[track_caller]
pub fn rememberEventStream<T, K, R, S>(key: K, subscribe: S) -> EventStream<T>
where
T: Send + 'static,
K: PartialEq + 'static,
R: 'static,
S: FnOnce(EventSender<T>) -> R + 'static,
{
let bridge = remember(Bridge::<T>::new);
let (id, stream) = bridge.with(|bridge| (bridge.id, bridge.channel.stream()));
#[cfg(not(target_arch = "wasm32"))]
let dispatcher = current_runtime_handle().map(|runtime| runtime.dispatcher());
crate::__disposable_effect_impl(crate::caller_location_key(), key, move |scope| {
#[cfg(not(target_arch = "wasm32"))]
let Some(dispatcher) = dispatcher else {
log::warn!("cranpose: an event stream was remembered without a runtime");
return scope.on_dispose(|| {});
};
let registration = subscribe(EventSender {
#[cfg(not(target_arch = "wasm32"))]
dispatcher,
bridge: id,
_events: std::marker::PhantomData,
});
scope.on_dispose(move || drop(registration))
});
stream
}
#[allow(non_snake_case)]
pub async fn withBlocking<T, F>(work: F) -> T
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static,
{
#[cfg(not(target_arch = "wasm32"))]
{
let slot: Arc<Mutex<Option<T>>> = Arc::new(Mutex::new(None));
let done = Arc::new(AtomicBool::new(false));
let wakers: Arc<Mutex<Vec<Waker>>> = Arc::new(Mutex::new(Vec::new()));
let worker_slot = Arc::clone(&slot);
let worker_done = Arc::clone(&done);
let worker_wakers = Arc::clone(&wakers);
BlockingPool::get().submit(Box::new(move || {
let value = work();
*worker_slot
.lock()
.unwrap_or_else(|error| error.into_inner()) = Some(value);
worker_done.store(true, Ordering::Release);
for waker in worker_wakers
.lock()
.unwrap_or_else(|error| error.into_inner())
.drain(..)
{
waker.wake();
}
}));
BlockingWork { slot, done, wakers }.await
}
#[cfg(target_arch = "wasm32")]
{
work()
}
}
#[allow(non_snake_case)]
pub fn launchBlocking<T>(work: impl FnOnce() -> T + Send + 'static, on_ui: impl FnOnce(T) + 'static)
where
T: Send + 'static,
{
let Some(runtime) = current_runtime_handle() else {
on_ui(work());
return;
};
let Some(continuation) = runtime.register_ui_cont(on_ui) else {
return;
};
let dispatcher = runtime.dispatcher();
#[cfg(not(target_arch = "wasm32"))]
BlockingPool::get().submit(Box::new(move || {
dispatcher.post_invoke(continuation, work());
}));
#[cfg(target_arch = "wasm32")]
dispatcher.post_invoke(continuation, work());
}
#[cfg(not(target_arch = "wasm32"))]
struct BlockingPool {
sender: std::sync::mpsc::Sender<BlockingJob>,
receiver: Arc<Mutex<std::sync::mpsc::Receiver<BlockingJob>>>,
state: Arc<Mutex<PoolState>>,
}
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Default)]
struct PoolState {
alive: usize,
outstanding: usize,
}
#[cfg(not(target_arch = "wasm32"))]
type BlockingJob = Box<dyn FnOnce() + Send + 'static>;
#[cfg(not(target_arch = "wasm32"))]
const MAX_BLOCKING_WORKERS: usize = 64;
#[cfg(not(target_arch = "wasm32"))]
const _: () = assert!(MAX_BLOCKING_WORKERS > 0 && MAX_BLOCKING_WORKERS <= 256);
#[cfg(not(target_arch = "wasm32"))]
impl BlockingPool {
fn get() -> &'static BlockingPool {
static POOL: OnceLock<BlockingPool> = OnceLock::new();
POOL.get_or_init(BlockingPool::new)
}
fn new() -> BlockingPool {
let (sender, receiver) = std::sync::mpsc::channel();
BlockingPool {
sender,
receiver: Arc::new(Mutex::new(receiver)),
state: Arc::new(Mutex::new(PoolState::default())),
}
}
fn submit(&self, job: BlockingJob) {
if self.take_slot() {
self.start_worker();
}
if let Err(returned) = self.sender.send(job) {
self.release_slot();
(returned.0)();
}
}
fn take_slot(&self) -> bool {
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
state.outstanding += 1;
let grow = state.alive < state.outstanding && state.alive < MAX_BLOCKING_WORKERS;
if grow {
state.alive += 1;
}
grow
}
fn release_slot(&self) {
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
state.outstanding = state.outstanding.saturating_sub(1);
}
fn start_worker(&self) {
let receiver = Arc::clone(&self.receiver);
let counters = Arc::clone(&self.state);
let started = std::thread::Builder::new()
.name("cranpose-blocking".to_string())
.spawn(move || {
loop {
let job = {
let queue = receiver.lock().unwrap_or_else(|error| error.into_inner());
queue.recv()
};
let Ok(job) = job else {
break;
};
job();
let mut counters = counters.lock().unwrap_or_else(|error| error.into_inner());
counters.outstanding = counters.outstanding.saturating_sub(1);
}
});
if started.is_err() {
let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
state.alive -= 1;
}
}
}
#[cfg(not(target_arch = "wasm32"))]
struct BlockingWork<T> {
slot: Arc<Mutex<Option<T>>>,
done: Arc<AtomicBool>,
wakers: Arc<Mutex<Vec<Waker>>>,
}
#[cfg(not(target_arch = "wasm32"))]
impl<T> Future for BlockingWork<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<T> {
if self.done.load(Ordering::Acquire)
&& let Some(value) = self
.slot
.lock()
.unwrap_or_else(|error| error.into_inner())
.take()
{
return Poll::Ready(value);
}
self.wakers
.lock()
.unwrap_or_else(|error| error.into_inner())
.push(context.waker().clone());
if self.done.load(Ordering::Acquire)
&& let Some(value) = self
.slot
.lock()
.unwrap_or_else(|error| error.into_inner())
.take()
{
return Poll::Ready(value);
}
Poll::Pending
}
}
#[allow(non_snake_case)]
#[track_caller]
pub fn produceState<T, K, F>(initial: T, key: K, producer: F) -> State<T>
where
T: Clone + 'static,
K: PartialEq + 'static,
F: FnOnce(ProduceScope<T>) -> Pin<Box<dyn Future<Output = ()>>> + 'static,
{
let state = remember(|| mutableStateOf(initial)).with(|state| *state);
let handle = ProduceScope { state };
crate::__launched_effect_async_impl(
crate::caller_location_key(),
std::panic::Location::caller().into(),
key,
move |_scope| producer(handle),
);
state.as_state()
}
pub struct ProduceScope<T: Clone + 'static> {
state: MutableState<T>,
}
impl<T: Clone + 'static> ProduceScope<T> {
pub fn set(&self, value: T) {
self.state.set(value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_delay_resolves_after_its_deadline() {
let started = Instant::now();
pollster::block_on(delay(Duration::from_millis(30)));
assert!(started.elapsed() >= Duration::from_millis(25));
}
#[test]
fn many_delays_share_one_timer_and_all_fire() {
let started = Instant::now();
pollster::block_on(async {
for _ in 0..4 {
delay(Duration::from_millis(5)).await;
}
});
assert!(started.elapsed() >= Duration::from_millis(15));
}
#[test]
fn an_elapsed_delay_is_ready_without_arming_the_timer() {
let mut future = Box::pin(Delay {
deadline: Instant::now() - Duration::from_millis(1),
armed: false,
fired: Arc::new(AtomicBool::new(false)),
});
let waker = Waker::noop().clone();
assert!(
future
.as_mut()
.poll(&mut Context::from_waker(&waker))
.is_ready()
);
}
}
#[cfg(test)]
mod stream_tests {
use super::*;
#[test]
fn a_channel_wakes_its_collector_and_ends_when_closed() {
let channel: EventChannel<u32> = EventChannel::new();
let stream = channel.stream();
let mut pending = Box::pin(stream.next());
let waker = Waker::noop().clone();
let mut context = Context::from_waker(&waker);
assert!(pending.as_mut().poll(&mut context).is_pending());
channel.send(7);
assert_eq!(pending.as_mut().poll(&mut context), Poll::Ready(Some(7)));
channel.send(8);
channel.close();
assert_eq!(pollster::block_on(stream.next()), Some(8));
assert_eq!(pollster::block_on(stream.next()), None);
assert_eq!(stream.delivered(), 2);
}
#[test]
fn an_event_goes_to_exactly_one_collector() {
let channel: EventChannel<u32> = EventChannel::new();
let first = channel.stream();
let second = first.clone();
channel.send(1);
channel.close();
assert_eq!(pollster::block_on(first.next()), Some(1));
assert_eq!(pollster::block_on(second.next()), None);
}
#[test]
fn sending_after_close_is_ignored() {
let channel: EventChannel<u32> = EventChannel::new();
let stream = channel.stream();
channel.close();
channel.send(1);
assert_eq!(pollster::block_on(stream.next()), None);
assert_eq!(channel.pending(), 0);
}
#[test]
fn blocking_work_resolves_with_its_result() {
let doubled = pollster::block_on(withBlocking(|| 21 * 2));
assert_eq!(doubled, 42);
}
}
#[cfg(test)]
mod timer_race_tests {
use super::*;
#[test]
fn concurrent_arming_never_loses_a_wake_up() {
let rounds = 40;
let threads: Vec<_> = (0..8)
.map(|worker| {
std::thread::spawn(move || {
for round in 0..rounds {
let millis = 1 + ((worker + round) % 5) as u64;
pollster::block_on(delay(Duration::from_millis(millis)));
}
})
})
.collect();
for thread in threads {
thread.join().expect("every waiter is woken");
}
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn blocking_work_reuses_its_threads_instead_of_one_per_call() {
use std::{collections::HashSet, sync::mpsc};
let pool = BlockingPool::new();
let (sender, receiver) = mpsc::channel();
for _ in 0..16 {
let done = Arc::new((Mutex::new(false), Condvar::new()));
let waiter = Arc::clone(&done);
let sender = sender.clone();
pool.submit(Box::new(move || {
let _ = sender.send(std::thread::current().id());
let (lock, signal) = &*done;
*lock.lock().unwrap_or_else(|error| error.into_inner()) = true;
signal.notify_all();
}));
let (lock, signal) = &*waiter;
let mut finished = lock.lock().unwrap_or_else(|error| error.into_inner());
while !*finished {
finished = signal
.wait(finished)
.unwrap_or_else(|error| error.into_inner());
}
}
drop(sender);
let threads: HashSet<_> = receiver.iter().collect();
assert!(
threads.len() < 16,
"sixteen serial jobs used {} threads; the pool is not reusing them",
threads.len()
);
}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn blocking_work_grows_so_one_slow_job_cannot_hold_up_another() {
let pool = BlockingPool::new();
let started = Arc::new((Mutex::new(0usize), Condvar::new()));
let release = Arc::new((Mutex::new(false), Condvar::new()));
for _ in 0..4 {
let started = Arc::clone(&started);
let release = Arc::clone(&release);
pool.submit(Box::new(move || {
{
let (count, signal) = &*started;
*count.lock().unwrap_or_else(|error| error.into_inner()) += 1;
signal.notify_all();
}
let (held, signal) = &*release;
let mut go = held.lock().unwrap_or_else(|error| error.into_inner());
while !*go {
go = signal.wait(go).unwrap_or_else(|error| error.into_inner());
}
}));
}
let (count, signal) = &*started;
let mut running = count.lock().unwrap_or_else(|error| error.into_inner());
while *running < 4 {
running = signal
.wait(running)
.unwrap_or_else(|error| error.into_inner());
}
drop(running);
let (held, signal) = &*release;
*held.lock().unwrap_or_else(|error| error.into_inner()) = true;
signal.notify_all();
}
}