use crate::effect::flush_effects;
use std::cell::RefCell;
use std::sync::{Condvar, Mutex, OnceLock};
thread_local! {
static SUPPRESS_UI_UPDATES: RefCell<bool> = const { RefCell::new(false) };
}
thread_local! {
static TRANSACTION_DEPTH: RefCell<usize> = const { RefCell::new(0) };
}
static TRANSACTION_BARRIER: OnceLock<TransactionBarrier> = OnceLock::new();
struct TransactionBarrier {
active_count: Mutex<usize>,
condvar: Condvar,
}
impl TransactionBarrier {
fn new() -> Self {
Self {
active_count: Mutex::new(0),
condvar: Condvar::new(),
}
}
fn enter(&self) {
let mut count = self.active_count.lock().unwrap();
*count += 1;
}
fn exit(&self) {
let mut count = self.active_count.lock().unwrap();
*count = count.saturating_sub(1);
if *count == 0 {
self.condvar.notify_all();
}
}
fn wait_until_clear(&self) {
let mut count = self.active_count.lock().unwrap();
while *count > 0 {
count = self.condvar.wait(count).unwrap();
}
}
}
fn get_barrier() -> &'static TransactionBarrier {
TRANSACTION_BARRIER.get_or_init(TransactionBarrier::new)
}
pub fn wait_for_transactions() {
get_barrier().wait_until_clear();
}
struct TransactionGuard {
prev_suppress: bool,
}
impl Drop for TransactionGuard {
fn drop(&mut self) {
SUPPRESS_UI_UPDATES.with(|s| {
*s.borrow_mut() = self.prev_suppress;
});
exit_transaction_inner();
}
}
fn exit_transaction_inner() {
let should_flush = TRANSACTION_DEPTH.with(|d| {
let mut depth = d.borrow_mut();
*depth = depth.saturating_sub(1);
*depth == 0
});
if should_flush {
get_barrier().exit();
flush_effects();
crate::effect::schedule_effect_processing();
}
}
pub fn is_transaction_active() -> bool {
TRANSACTION_DEPTH.with(|d| *d.borrow() > 0)
}
fn enter_transaction() {
TRANSACTION_DEPTH.with(|d| {
let depth = *d.borrow();
if depth == 0 {
get_barrier().enter();
}
*d.borrow_mut() = depth + 1;
});
}
#[cfg(test)]
fn is_ui_update_suppressed() -> bool {
SUPPRESS_UI_UPDATES.with(|s| *s.borrow())
}
pub struct Transaction {
_suppress_ui: bool,
}
impl Transaction {
pub fn run<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
Self::run_with_suppress(f, false)
}
pub fn no_ui_updates<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
Self::run_with_suppress(f, true)
}
fn run_with_suppress<F, R>(f: F, suppress_ui: bool) -> R
where
F: FnOnce() -> R,
{
let (prev_suppress, should_suppress) = SUPPRESS_UI_UPDATES.with(|s| {
let old = *s.borrow();
let new = old || suppress_ui; (old, new)
});
SUPPRESS_UI_UPDATES.with(|s| {
*s.borrow_mut() = should_suppress;
});
enter_transaction();
let _guard = TransactionGuard { prev_suppress };
f()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transaction_suppresses_ui_updates() {
assert!(!is_ui_update_suppressed());
Transaction::no_ui_updates(|| {
assert!(is_ui_update_suppressed());
});
assert!(!is_ui_update_suppressed());
}
#[test]
fn transaction_nesting_preserves_state() {
assert!(!is_ui_update_suppressed());
Transaction::no_ui_updates(|| {
assert!(is_ui_update_suppressed());
Transaction::run(|| {
assert!(is_ui_update_suppressed());
});
assert!(is_ui_update_suppressed());
});
assert!(!is_ui_update_suppressed());
}
#[test]
fn transaction_run_allows_ui_updates() {
assert!(!is_ui_update_suppressed());
Transaction::run(|| {
assert!(!is_ui_update_suppressed());
});
assert!(!is_ui_update_suppressed());
}
#[test]
fn transaction_returns_value() {
let result = Transaction::run(|| 42);
assert_eq!(result, 42);
}
#[test]
fn transaction_no_ui_updates_returns_value() {
let result = Transaction::no_ui_updates(|| "hello");
assert_eq!(result, "hello");
}
}