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, empty_owner, under_owner};
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()>, OwnerId);
struct EffectSlot {
callback: Option<Box<dyn FnMut()>>,
scope: OwnerId,
dependencies: Vec<SignalId>,
generation: u32,
state: EffectState,
paused: bool,
missed: bool,
memo: bool,
}
#[derive(Default)]
struct EffectSubscribers {
handles: Vec<u32>,
lists: Vec<Vec<EffectId>>,
free: Vec<u32>,
}
impl EffectSubscribers {
fn handle(&self, signal_id: SignalId) -> Option<usize> {
match self.handles.get(signal_id.index()) {
Some(&handle) if handle != 0 => Some(handle as usize - 1),
_ => None,
}
}
fn get(&self, signal_id: SignalId) -> Option<&Vec<EffectId>> {
self.handle(signal_id).map(|list| &self.lists[list])
}
fn get_mut(&mut self, signal_id: SignalId) -> Option<&mut Vec<EffectId>> {
self.handle(signal_id).map(|list| &mut self.lists[list])
}
fn get_or_insert(&mut self, signal_id: SignalId) -> &mut Vec<EffectId> {
let list = match self.handle(signal_id) {
Some(list) => list,
None => {
let list = self.free.pop().unwrap_or_else(|| {
self.lists.push(Vec::new());
self.lists.len() as u32 - 1
});
if signal_id.index() >= self.handles.len() {
self.handles.resize(signal_id.index() + 1, 0);
}
self.handles[signal_id.index()] = list + 1;
list as usize
}
};
&mut self.lists[list]
}
fn remove(&mut self, signal_id: SignalId) -> Option<Vec<EffectId>> {
let list = self.handle(signal_id)?;
self.handles[signal_id.index()] = 0;
self.free.push(list as u32);
Some(std::mem::take(&mut self.lists[list]))
}
fn unsubscribe(&mut self, signal_id: SignalId, effect_id: EffectId) {
if let Some(subs) = self.get_mut(signal_id) {
vec_remove(subs, &effect_id);
if subs.is_empty() {
self.remove(signal_id);
}
}
}
#[cfg(test)]
fn len(&self) -> usize {
self.lists.len() - self.free.len()
}
}
#[derive(Default)]
struct PendingEffects {
memos: VecDeque<EffectId>,
effects: VecDeque<EffectId>,
}
impl PendingEffects {
fn queue(&mut self, memo: bool) -> &mut VecDeque<EffectId> {
if memo {
&mut self.memos
} else {
&mut self.effects
}
}
fn push(&mut self, effect_id: EffectId, memo: bool) {
let queue = self.queue(memo);
if !queue.contains(&effect_id) {
queue.push_back(effect_id);
}
}
fn pop(&mut self) -> Option<EffectId> {
self.memos.pop_front().or_else(|| self.effects.pop_front())
}
fn remove(&mut self, effect_id: EffectId, memo: bool) {
let queue = self.queue(memo);
if let Some(pos) = queue.iter().position(|e| *e == effect_id) {
queue.remove(pos);
}
}
}
#[derive(Default)]
pub struct Runtime {
pending: PendingEffects,
effects: Vec<EffectSlot>,
free_effect_indices: Vec<u32>,
signal_subscribers: EffectSubscribers,
}
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) {
self.signal_subscribers.remove(id);
}
pub fn dispose_signal_subscriptions(&mut self, id: SignalId) {
let Some(subs) = self.signal_subscribers.remove(id) else {
return;
};
for effect_id in 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()>,
scope: OwnerId,
memo: bool,
) -> EffectId {
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;
slot.memo = memo;
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,
memo,
});
EffectId {
index,
generation: 0,
}
}
fn enqueue_subscribers(&mut self, signal_id: SignalId) {
let Self {
effects,
signal_subscribers,
pending,
..
} = self;
let Some(subs) = signal_subscribers.get(signal_id) else {
return;
};
for &effect_id in subs {
let memo = effects[effect_id.index()].memo;
pending.push(effect_id, memo);
}
}
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) {
let memo = slot.memo;
self.pending.push(effect_id, memo);
}
}
}
}
fn begin_effect(&mut self, effect_id: EffectId) -> Option<EffectRun> {
let Self {
effects,
signal_subscribers,
..
} = self;
let slot = effects
.get_mut(effect_id.index())
.filter(|slot| slot.generation == effect_id.generation)?;
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;
for &signal_id in &slot.dependencies {
if let Some(subs) = signal_subscribers.get_mut(signal_id) {
vec_remove(subs, &effect_id);
}
}
slot.dependencies.clear();
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 {
vec_insert(self.signal_subscribers.get_or_insert(signal_id), 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;
}
Some(slot.scope)
}
pub fn dispose_effect(&mut self, effect_id: EffectId) {
let Some(slot) = self.effect_slot_mut(effect_id) else {
return;
};
let memo = slot.memo;
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 {
self.signal_subscribers.unsubscribe(signal_id, effect_id);
}
self.pending.remove(effect_id, memo);
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 {
self.signal_subscribers.unsubscribe(signal_id, effect_id);
}
self.pending.remove(effect_id, memo);
}
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(|| {
empty_owner(scope);
under_owner(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.pending.pop()) {
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;
fn effect_index_entries() -> usize {
with_runtime(|rt| rt.signal_subscribers.len())
}
#[test]
fn only_a_signal_an_effect_reads_has_a_list() {
let before = effect_index_entries();
let signals: Vec<_> = (0..1000).map(|_| create_signal(0u32)).collect();
assert_eq!(effect_index_entries(), before, "nothing has read them");
let last = *signals.last().unwrap();
create_effect(move || {
last.get();
});
assert_eq!(effect_index_entries(), before + 1, "one reader, one list");
}
#[test]
fn a_list_goes_back_when_its_signal_or_its_last_reader_goes() {
use crate::reactive::owner::{dispose_owner_now, with_owner};
let before = effect_index_entries();
let (signal, signal_scope) = with_owner(|| create_signal(0u32));
let read = move || {
signal.get();
};
let ((), reader) = with_owner(|| create_effect(read));
assert_eq!(effect_index_entries(), before + 1, "the control: one list");
dispose_owner_now(reader);
assert_eq!(
effect_index_entries(),
before,
"its only reader went, and the list with it"
);
let ((), reader) = with_owner(|| create_effect(read));
assert_eq!(effect_index_entries(), before + 1);
dispose_owner_now(signal_scope);
assert_eq!(
effect_index_entries(),
before,
"the signal went, and its list with it"
);
dispose_owner_now(reader);
}
#[test]
fn a_signal_disposed_by_the_run_that_read_it_leaves_its_slot_unsubscribed() {
use crate::reactive::owner::{dispose_owner_now, with_owner};
let (doomed, scope) = with_owner(|| create_signal(0u32));
let runs = Rc::new(Cell::new(0));
let counter = runs.clone();
create_effect(move || {
counter.set(counter.get() + 1);
if counter.get() == 1 {
doomed.get();
dispose_owner_now(scope);
}
});
assert_eq!(runs.get(), 1);
let successor = create_signal(0u32);
successor.set(1);
assert_eq!(runs.get(), 1, "the effect never read the successor");
}
#[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);
}
}