use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;
#[cfg(any(test, feature = "test"))]
mod flush_wakers {
use std::cell::RefCell;
use std::task::Waker;
thread_local! {
static FLUSH_WAKERS: RefCell<Vec<Waker>> = Default::default();
}
#[cfg(all(
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "not_browser_env")
))]
pub(super) fn register(waker: Waker) {
FLUSH_WAKERS.with(|w| {
w.borrow_mut().push(waker);
});
}
pub(super) fn wake_all() {
FLUSH_WAKERS.with(|w| {
for waker in w.borrow_mut().drain(..) {
waker.wake();
}
});
}
}
pub type Shared<T> = Rc<RefCell<T>>;
pub trait Runnable {
fn run(self: Box<Self>);
}
struct QueueEntry {
task: Box<dyn Runnable>,
}
#[derive(Default)]
struct FifoQueue {
inner: Vec<QueueEntry>,
}
impl FifoQueue {
fn push(&mut self, task: Box<dyn Runnable>) {
self.inner.push(QueueEntry { task });
}
fn drain_into(&mut self, queue: &mut Vec<QueueEntry>) {
queue.append(&mut self.inner);
}
}
#[derive(Default)]
struct TopologicalQueue {
inner: BTreeMap<usize, QueueEntry>,
}
impl TopologicalQueue {
#[cfg(any(feature = "ssr", feature = "csr"))]
fn push(&mut self, component_id: usize, task: Box<dyn Runnable>) {
self.inner.insert(component_id, QueueEntry { task });
}
#[inline]
fn pop_topmost(&mut self) -> Option<QueueEntry> {
self.inner.pop_first().map(|(_, v)| v)
}
fn drain_post_order_into(&mut self, queue: &mut Vec<QueueEntry>) {
if self.inner.is_empty() {
return;
}
let rendered = std::mem::take(&mut self.inner);
queue.extend(rendered.into_values().rev());
}
}
#[derive(Default)]
#[allow(missing_debug_implementations)] struct Scheduler {
main: FifoQueue,
destroy: FifoQueue,
create: FifoQueue,
props_update: FifoQueue,
update: FifoQueue,
render: TopologicalQueue,
render_first: TopologicalQueue,
render_priority: TopologicalQueue,
rendered_first: TopologicalQueue,
rendered: TopologicalQueue,
}
#[inline]
fn with<R>(f: impl FnOnce(&mut Scheduler) -> R) -> R {
thread_local! {
static SCHEDULER: RefCell<Scheduler> = Default::default();
}
SCHEDULER.with(|s| f(&mut s.borrow_mut()))
}
pub fn push(runnable: Box<dyn Runnable>) {
with(|s| s.main.push(runnable));
start();
}
#[cfg(any(feature = "ssr", feature = "csr"))]
mod feat_csr_ssr {
use super::*;
pub(crate) fn push_component_create(
component_id: usize,
create: Box<dyn Runnable>,
first_render: Box<dyn Runnable>,
) {
with(|s| {
s.create.push(create);
s.render_first.push(component_id, first_render);
});
}
pub(crate) fn push_component_destroy(runnable: Box<dyn Runnable>) {
with(|s| s.destroy.push(runnable));
}
pub(crate) fn push_component_render(component_id: usize, render: Box<dyn Runnable>) {
with(|s| {
s.render.push(component_id, render);
});
}
pub(crate) fn push_component_update(runnable: Box<dyn Runnable>) {
with(|s| s.update.push(runnable));
}
}
#[cfg(any(feature = "ssr", feature = "csr"))]
pub(crate) use feat_csr_ssr::*;
#[cfg(feature = "csr")]
mod feat_csr {
use super::*;
pub(crate) fn push_component_rendered(
component_id: usize,
rendered: Box<dyn Runnable>,
first_render: bool,
) {
with(|s| {
if first_render {
s.rendered_first.push(component_id, rendered);
} else {
s.rendered.push(component_id, rendered);
}
});
}
pub(crate) fn push_component_props_update(props_update: Box<dyn Runnable>) {
with(|s| s.props_update.push(props_update));
}
}
#[cfg(feature = "csr")]
pub(crate) use feat_csr::*;
#[cfg(feature = "hydration")]
mod feat_hydration {
use super::*;
pub(crate) fn push_component_priority_render(component_id: usize, render: Box<dyn Runnable>) {
with(|s| {
s.render_priority.push(component_id, render);
});
}
}
#[cfg(feature = "hydration")]
pub(crate) use feat_hydration::*;
pub(crate) fn start_now() {
#[tracing::instrument(level = tracing::Level::DEBUG)]
fn scheduler_loop() {
let mut queue = vec![];
loop {
with(|s| s.fill_queue(&mut queue));
if queue.is_empty() {
break;
}
for r in queue.drain(..) {
r.task.run();
}
}
}
thread_local! {
static LOCK: RefCell<()> = Default::default();
}
LOCK.with(|l| {
if let Ok(_lock) = l.try_borrow_mut() {
scheduler_loop();
#[cfg(any(test, feature = "test"))]
flush_wakers::wake_all();
}
});
}
#[cfg(all(
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "not_browser_env")
))]
mod arch {
use std::sync::atomic::{AtomicBool, Ordering};
use wasm_bindgen::prelude::*;
use crate::platform::spawn_local;
static IS_SCHEDULED: AtomicBool = AtomicBool::new(false);
fn check_scheduled() -> bool {
IS_SCHEDULED.load(Ordering::Relaxed)
}
fn set_scheduled(is: bool) {
IS_SCHEDULED.store(is, Ordering::Relaxed)
}
#[cfg(any(test, feature = "test"))]
pub(super) fn is_scheduled() -> bool {
check_scheduled()
}
const YIELD_DEADLINE_MS: f64 = 16.0;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_name = setTimeout)]
fn set_timeout(handler: &js_sys::Function, timeout: i32) -> i32;
}
fn run_scheduler(mut queue: Vec<super::QueueEntry>) {
let deadline = js_sys::Date::now() + YIELD_DEADLINE_MS;
loop {
super::with(|s| s.fill_queue(&mut queue));
if queue.is_empty() {
break;
}
for r in queue.drain(..) {
r.task.run();
}
if js_sys::Date::now() >= deadline {
let can_yield = super::with(|s| s.can_yield());
if can_yield {
let cb = Closure::once_into_js(move || run_scheduler(queue));
set_timeout(cb.unchecked_ref(), 0);
return;
}
}
}
set_scheduled(false);
#[cfg(any(test, feature = "test"))]
super::flush_wakers::wake_all();
}
pub(crate) fn start() {
if check_scheduled() {
return;
}
set_scheduled(true);
spawn_local(async {
run_scheduler(vec![]);
});
}
}
#[cfg(any(
not(target_arch = "wasm32"),
target_os = "wasi",
feature = "not_browser_env"
))]
mod arch {
pub(crate) fn start() {
super::start_now();
}
}
pub(crate) use arch::*;
#[cfg(all(
any(test, feature = "test"),
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "not_browser_env")
))]
pub async fn flush() {
std::future::poll_fn(|cx| {
start_now();
if arch::is_scheduled() {
flush_wakers::register(cx.waker().clone());
std::task::Poll::Pending
} else {
std::task::Poll::Ready(())
}
})
.await
}
#[cfg(all(
any(test, feature = "test"),
not(all(
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "not_browser_env")
))
))]
pub async fn flush() {
start_now();
}
impl Scheduler {
#[cfg(all(
target_arch = "wasm32",
not(target_os = "wasi"),
not(feature = "not_browser_env")
))]
fn can_yield(&self) -> bool {
self.destroy.inner.is_empty()
&& self.create.inner.is_empty()
&& self.render_first.inner.is_empty()
&& self.render.inner.is_empty()
&& self.render_priority.inner.is_empty()
}
fn fill_queue(&mut self, to_run: &mut Vec<QueueEntry>) {
self.destroy.drain_into(to_run);
self.create.drain_into(to_run);
if !to_run.is_empty() {
return;
}
if let Some(r) = self.render_first.pop_topmost() {
to_run.push(r);
return;
}
self.props_update.drain_into(to_run);
if let Some(r) = self.render_priority.pop_topmost() {
to_run.push(r);
return;
}
self.rendered_first.drain_post_order_into(to_run);
self.update.drain_into(to_run);
self.main.drain_into(to_run);
if !to_run.is_empty() {
return;
}
if let Some(r) = self.render.pop_topmost() {
to_run.push(r);
return;
}
self.rendered.drain_post_order_into(to_run);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_executes_runnables_immediately() {
use std::cell::Cell;
thread_local! {
static FLAG: Cell<bool> = Default::default();
}
struct Test;
impl Runnable for Test {
fn run(self: Box<Self>) {
FLAG.with(|v| v.set(true));
}
}
push(Box::new(Test));
FLAG.with(|v| assert!(v.get()));
}
}