use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use smallvec::SmallVec;
use super::invalidation::suspend_widget_tracking;
use super::owner::{OwnerId, current_owner, under_scope};
use super::state::with_reactive;
pub(crate) type EffectReads = SmallVec<[SignalId; 4]>;
static WRITE_EPOCH: AtomicU64 = AtomicU64::new(0);
type EpochWrite = (u64, Box<dyn FnOnce() + Send>);
static WRITE_QUEUE: Mutex<Vec<EpochWrite>> = Mutex::new(Vec::new());
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct SignalId {
index: u32,
generation: u32,
}
impl SignalId {
pub(crate) fn new(index: u32, generation: u32) -> Self {
Self { index, generation }
}
#[inline]
pub(crate) fn index(self) -> usize {
self.index as usize
}
#[inline]
pub(crate) fn generation(self) -> u32 {
self.generation
}
}
impl std::fmt::Display for SignalId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}v{}", self.index, self.generation)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct EffectId {
index: u32,
generation: u32,
}
impl EffectId {
#[inline]
fn index(self) -> usize {
self.index as usize
}
}
impl std::fmt::Display for EffectId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}v{}", self.index, self.generation)
}
}
fn vec_insert<T: PartialEq>(vec: &mut Vec<T>, value: T) {
if !vec.contains(&value) {
vec.push(value);
}
}
fn vec_remove<T: PartialEq>(vec: &mut Vec<T>, value: &T) {
if let Some(pos) = vec.iter().position(|x| x == value) {
vec.swap_remove(pos);
}
}
#[cfg(debug_assertions)]
pub(crate) fn effect_tracking_active() -> bool {
with_reactive(|reactive| {
reactive
.effect_tracking
.try_borrow()
.map(|stack| !stack.is_empty())
.unwrap_or(true)
})
}
pub fn record_effect_read(signal_id: SignalId) {
with_reactive(|reactive| {
if let Ok(mut s) = reactive.effect_tracking.try_borrow_mut()
&& let Some(entry) = s.last_mut()
&& !entry.1.contains(&signal_id)
{
entry.1.push(signal_id);
}
});
}
pub(crate) fn suspend_effect_tracking<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let saved: Vec<_> =
with_reactive(|reactive| reactive.effect_tracking.borrow_mut().drain(..).collect());
let _guard = super::guard::defer(move || {
with_reactive(|reactive| *reactive.effect_tracking.borrow_mut() = saved);
});
f()
}
pub(crate) fn current_write_epoch() -> u64 {
WRITE_EPOCH.load(Ordering::Acquire)
}
pub fn queue_bg_write(epoch: u64, f: impl FnOnce() + Send + 'static) {
if let Ok(mut q) = WRITE_QUEUE.lock() {
q.push((epoch, Box::new(f)));
}
crate::ingress::notify(crate::ingress::IngressMessage::BgWritesQueued);
}
pub fn flush_bg_writes() {
let current_epoch = WRITE_EPOCH.load(Ordering::Acquire);
loop {
let writes: Vec<(u64, Box<dyn FnOnce() + Send>)> = match WRITE_QUEUE.lock() {
Ok(mut q) if !q.is_empty() => q.drain(..).collect(),
_ => return,
};
let mut executed = 0usize;
let mut stale = 0usize;
for (epoch, write_fn) in writes {
if epoch == current_epoch {
write_fn();
executed += 1;
} else {
stale += 1;
}
}
if stale > 0 {
log::debug!(
"flush_bg_writes: dropped {} stale writes (old epoch), executed {}",
stale,
executed
);
} else if executed > 0 {
log::trace!("flush_bg_writes: processed {} queued writes", executed);
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
enum EffectState {
#[default]
Vacant,
Idle,
Running,
DisposedWhileRunning,
}
type EffectRun = (Box<dyn FnMut()>, Option<OwnerId>);
#[derive(Default)]
struct EffectSlot {
callback: Option<Box<dyn FnMut()>>,
scope: Option<OwnerId>,
dependencies: Vec<SignalId>,
generation: u32,
state: EffectState,
paused: bool,
missed: bool,
}
#[derive(Default)]
pub struct Runtime {
pending_effects: VecDeque<EffectId>,
effects: Vec<EffectSlot>,
free_effect_indices: Vec<u32>,
signal_subscribers: Vec<Vec<EffectId>>,
}
impl Runtime {
fn effect_slot_mut(&mut self, id: EffectId) -> Option<&mut EffectSlot> {
self.effects
.get_mut(id.index())
.filter(|slot| slot.generation == id.generation)
}
pub fn register_signal(&mut self, id: SignalId) {
while self.signal_subscribers.len() <= id.index() {
self.signal_subscribers.push(Vec::new());
}
self.signal_subscribers[id.index()].clear();
}
pub fn dispose_signal_subscriptions(&mut self, id: SignalId) {
let Some(subs) = self.signal_subscribers.get_mut(id.index()) else {
return;
};
for effect_id in std::mem::take(subs) {
if let Some(slot) = self.effect_slot_mut(effect_id) {
vec_remove(&mut slot.dependencies, &id);
}
}
}
pub fn allocate_effect(&mut self, callback: Box<dyn FnMut()>) -> EffectId {
let scope = current_owner();
let paused = super::owner::current_owner_is_paused();
if let Some(index) = self.free_effect_indices.pop() {
let slot = &mut self.effects[index as usize];
slot.generation = slot.generation.wrapping_add(1);
slot.callback = Some(callback);
slot.scope = scope;
slot.dependencies.clear();
slot.state = EffectState::Idle;
slot.paused = paused;
slot.missed = false;
return EffectId {
index,
generation: slot.generation,
};
}
let index = self.effects.len() as u32;
self.effects.push(EffectSlot {
callback: Some(callback),
scope,
dependencies: Vec::new(),
generation: 0,
state: EffectState::Idle,
paused,
missed: false,
});
EffectId {
index,
generation: 0,
}
}
fn enqueue_subscribers(&mut self, signal_id: SignalId) {
let Some(subs) = self.signal_subscribers.get(signal_id.index()) else {
return;
};
for i in 0..subs.len() {
let effect_id = self.signal_subscribers[signal_id.index()][i];
self.enqueue(effect_id);
}
}
fn enqueue(&mut self, effect_id: EffectId) {
if !self.pending_effects.contains(&effect_id) {
self.pending_effects.push_back(effect_id);
}
}
fn pop_pending_effect(&mut self) -> Option<EffectId> {
self.pending_effects.pop_front()
}
pub(crate) fn set_effects_paused(&mut self, effects: &[EffectId], paused: bool) {
for &effect_id in effects {
if let Some(slot) = self.effect_slot_mut(effect_id) {
slot.paused = paused;
if !paused && std::mem::take(&mut slot.missed) {
self.enqueue(effect_id);
}
}
}
}
fn begin_effect(&mut self, effect_id: EffectId) -> Option<EffectRun> {
let slot = self.effect_slot_mut(effect_id)?;
if slot.state != EffectState::Idle {
return None;
}
if slot.paused {
slot.missed = true;
return None;
}
let callback = slot.callback.take()?;
let scope = slot.scope;
slot.state = EffectState::Running;
let old_deps = std::mem::take(&mut slot.dependencies);
for signal_id in old_deps {
if let Some(subs) = self.signal_subscribers.get_mut(signal_id.index()) {
vec_remove(subs, &effect_id);
}
}
Some((callback, scope))
}
fn finish_effect(
&mut self,
effect_id: EffectId,
callback: Box<dyn FnMut()>,
reads: EffectReads,
) {
let Some(slot) = self.effect_slot_mut(effect_id) else {
return; };
match slot.state {
EffectState::Running => {
slot.callback = Some(callback);
slot.state = EffectState::Idle;
for signal_id in reads {
if signal_id.index() < self.signal_subscribers.len() {
vec_insert(&mut self.signal_subscribers[signal_id.index()], effect_id);
if let Some(slot) = self.effect_slot_mut(effect_id) {
vec_insert(&mut slot.dependencies, signal_id);
}
}
}
}
EffectState::DisposedWhileRunning => {
drop(callback);
slot.state = EffectState::Vacant;
self.free_effect_indices.push(effect_id.index);
}
EffectState::Vacant | EffectState::Idle => {
drop(callback);
}
}
}
#[cfg(test)]
pub(crate) fn effect_scope(&self, effect_id: EffectId) -> Option<OwnerId> {
let slot = self.effects.get(effect_id.index as usize)?;
if slot.generation != effect_id.generation || slot.state == EffectState::Vacant {
return None;
}
slot.scope
}
pub fn dispose_effect(&mut self, effect_id: EffectId) {
let Some(slot) = self.effect_slot_mut(effect_id) else {
return;
};
match slot.state {
EffectState::Idle => {
slot.callback = None;
slot.state = EffectState::Vacant;
let deps = std::mem::take(&mut slot.dependencies);
for signal_id in deps {
if let Some(subs) = self.signal_subscribers.get_mut(signal_id.index()) {
vec_remove(subs, &effect_id);
}
}
if let Some(pos) = self.pending_effects.iter().position(|e| *e == effect_id) {
self.pending_effects.remove(pos);
}
self.free_effect_indices.push(effect_id.index);
}
EffectState::Running => {
slot.state = EffectState::DisposedWhileRunning;
let deps = std::mem::take(&mut slot.dependencies);
for signal_id in deps {
if let Some(subs) = self.signal_subscribers.get_mut(signal_id.index()) {
vec_remove(subs, &effect_id);
}
}
if let Some(pos) = self.pending_effects.iter().position(|e| *e == effect_id) {
self.pending_effects.remove(pos);
}
}
EffectState::Vacant | EffectState::DisposedWhileRunning => {
}
}
}
}
pub(crate) fn run_effect_by_id(effect_id: EffectId) {
let Some((mut callback, scope)) = with_runtime(|rt| rt.begin_effect(effect_id)) else {
return;
};
with_reactive(|reactive| {
reactive
.effect_tracking
.borrow_mut()
.push((effect_id, EffectReads::new()));
});
let panic_payload = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
under_scope(scope, || suspend_widget_tracking(&mut *callback));
}))
.err();
let reads = with_reactive(|reactive| reactive.effect_tracking.borrow_mut().pop())
.map(|(_eid, reads)| reads)
.unwrap_or_default();
with_runtime(|rt| rt.finish_effect(effect_id, callback, reads));
if let Some(payload) = panic_payload {
std::panic::resume_unwind(payload);
}
}
pub(crate) fn flush_pending_effects() {
if with_reactive(|reactive| reactive.flushing.replace(true)) {
return;
}
struct FlushGuard;
impl Drop for FlushGuard {
fn drop(&mut self) {
with_reactive(|reactive| reactive.flushing.set(false));
}
}
let _guard = FlushGuard;
while let Some(effect_id) = with_runtime(|rt| rt.pop_pending_effect()) {
run_effect_by_id(effect_id);
}
}
pub(crate) fn notify_write(signal_id: SignalId) {
with_runtime(|rt| rt.enqueue_subscribers(signal_id));
flush_unless_batching();
}
pub(crate) fn flush_unless_batching() {
let batching = with_reactive(|reactive| reactive.batch_depth.get() > 0);
if !batching {
flush_pending_effects();
}
}
pub fn with_runtime<F, R>(f: F) -> R
where
F: FnOnce(&mut Runtime) -> R,
{
with_reactive(|reactive| f(&mut reactive.runtime.borrow_mut()))
}
pub(crate) fn reset_bg_writes() {
WRITE_EPOCH.fetch_add(1, Ordering::Release);
if let Ok(mut q) = WRITE_QUEUE.lock() {
q.clear();
}
}
pub fn batch<R>(f: impl FnOnce() -> R) -> R {
with_reactive(|reactive| reactive.batch_depth.set(reactive.batch_depth.get() + 1));
let guard = super::guard::defer(|| {
with_reactive(|reactive| reactive.batch_depth.set(reactive.batch_depth.get() - 1));
});
let result = f();
drop(guard);
flush_unless_batching();
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::{create_effect, create_signal};
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn test_caught_panic_in_batch_does_not_wedge_effects() {
let sig = create_signal(0);
let observed = Rc::new(Cell::new(0));
let o = observed.clone();
create_effect(move || o.set(sig.get()));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
batch(|| {
sig.set(1);
panic!("boom");
})
}));
assert!(result.is_err());
assert_eq!(
with_reactive(|reactive| reactive.batch_depth.get()),
0,
"batch depth must unwind"
);
sig.set(2);
assert_eq!(observed.get(), 2);
}
#[test]
fn test_panicking_effect_survives_and_reruns() {
let sig = create_signal(0);
let observed = Rc::new(Cell::new(0));
let o = observed.clone();
create_effect(move || {
let v = sig.get();
o.set(v);
if v == 1 {
panic!("effect panic");
}
});
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| sig.set(1)));
assert!(result.is_err(), "panic must propagate out of set()");
assert_eq!(observed.get(), 1);
assert!(
!with_reactive(|reactive| reactive.flushing.get()),
"flush guard must have reset"
);
sig.set(2);
assert_eq!(observed.get(), 2);
}
}