#![forbid(unsafe_code)]
#![deny(missing_docs)]
use or_poisoned::OrPoisoned;
use reactive_graph::{
owner::{ArenaItem, LocalStorage, Storage, SyncStorage},
signal::{
guards::{Plain, ReadGuard, WriteGuard},
ArcTrigger,
},
traits::{
DefinedAt, Dispose, IsDisposed, Notify, ReadUntracked, Track,
UntrackableGuard, Write,
},
};
pub use reactive_stores_macro::{Patch, Store};
use rustc_hash::FxHashMap;
use std::{
any::Any,
fmt::Debug,
hash::Hash,
ops::DerefMut,
panic::Location,
sync::{Arc, RwLock},
};
mod arc_field;
mod deref;
mod field;
mod iter;
mod keyed;
mod len;
mod option;
mod patch;
mod path;
#[cfg(feature = "serde")]
mod serde;
#[cfg(feature = "slotmap")]
mod slotmap;
mod store_field;
mod subfield;
pub use arc_field::ArcField;
pub use deref::*;
pub use field::Field;
pub use iter::*;
pub use keyed::*;
pub use len::Len;
pub use option::*;
pub use patch::*;
pub use path::{StorePath, StorePathSegment};
pub use store_field::StoreField;
pub use subfield::Subfield;
#[derive(Debug, Default)]
struct TriggerMap(FxHashMap<StorePath, StoreFieldTrigger>);
#[derive(Debug, Clone, Default)]
pub struct StoreFieldTrigger {
pub(crate) this: ArcTrigger,
pub(crate) children: ArcTrigger,
}
impl StoreFieldTrigger {
pub fn new() -> Self {
Self::default()
}
}
impl TriggerMap {
fn get_or_insert(&mut self, key: StorePath) -> StoreFieldTrigger {
if let Some(trigger) = self.0.get(&key) {
trigger.clone()
} else {
let new = StoreFieldTrigger::new();
self.0.insert(key, new.clone());
new
}
}
#[allow(unused)]
fn remove(&mut self, key: &StorePath) -> Option<StoreFieldTrigger> {
self.0.remove(key)
}
}
pub struct FieldKeys<K> {
spare_keys: Vec<StorePathSegment>,
current_key: usize,
keys: FxHashMap<K, (StorePathSegment, usize)>,
}
impl<K> FieldKeys<K>
where
K: Debug + Hash + PartialEq + Eq,
{
pub fn new(from_keys: Vec<K>) -> Self {
let mut keys = FxHashMap::with_capacity_and_hasher(
from_keys.len(),
Default::default(),
);
for (idx, key) in from_keys.into_iter().enumerate() {
let segment = idx.into();
keys.insert(key, (segment, idx));
}
Self {
spare_keys: Vec::new(),
current_key: keys.len().saturating_sub(1),
keys,
}
}
}
impl<K> FieldKeys<K>
where
K: Hash + PartialEq + Eq,
{
#[doc(hidden)]
pub fn get(&self, key: &K) -> Option<(StorePathSegment, usize)> {
self.keys.get(key).copied()
}
fn next_key(&mut self) -> StorePathSegment {
self.spare_keys.pop().unwrap_or_else(|| {
self.current_key += 1;
self.current_key.into()
})
}
fn update(
&mut self,
iter: impl IntoIterator<Item = K>,
) -> Vec<(usize, StorePathSegment)> {
let new_keys = iter
.into_iter()
.enumerate()
.map(|(idx, key)| (key, idx))
.collect::<FxHashMap<K, usize>>();
let mut index_keys = Vec::with_capacity(new_keys.len());
self.keys.retain(|key, old_entry| match new_keys.get(key) {
Some(idx) => {
old_entry.1 = *idx;
true
}
None => {
self.spare_keys.push(old_entry.0);
false
}
});
for (key, idx) in new_keys {
match self.keys.get(&key) {
Some((segment, idx)) => index_keys.push((*idx, *segment)),
None => {
let path = self.next_key();
self.keys.insert(key, (path, idx));
index_keys.push((idx, path));
}
}
}
index_keys
}
}
impl<K> Default for FieldKeys<K> {
fn default() -> Self {
Self {
spare_keys: Default::default(),
current_key: Default::default(),
keys: Default::default(),
}
}
}
type Map<K, V> = Arc<std::sync::RwLock<std::collections::HashMap<K, V>>>;
#[derive(Clone, Default)]
pub struct KeyMap(
Map<StorePath, Box<dyn Any + Send + Sync>>,
Map<(StorePath, usize), StorePathSegment>,
);
impl KeyMap {
#[doc(hidden)]
pub fn with_field_keys<K, T>(
&self,
path: StorePath,
fun: impl FnOnce(&mut FieldKeys<K>) -> (T, Vec<(usize, StorePathSegment)>),
initialize: impl FnOnce() -> Vec<K>,
) -> Option<T>
where
K: Debug + Hash + PartialEq + Eq + Send + Sync + 'static,
{
let mut guard = self.0.write().or_poisoned();
let entry = guard
.entry(path.clone())
.or_insert_with(|| Box::new(FieldKeys::new(initialize())));
let entry = entry.downcast_mut::<FieldKeys<K>>()?;
let (result, new_keys) = fun(entry);
if !new_keys.is_empty() {
for (idx, segment) in new_keys {
self.1
.write()
.or_poisoned()
.insert((path.clone(), idx), segment);
}
}
Some(result)
}
fn contains_key(&self, key: &StorePath) -> bool {
self.0.read().or_poisoned().contains_key(key)
}
fn get_key_for_index(
&self,
key: &(StorePath, usize),
) -> Option<StorePathSegment> {
self.1.read().or_poisoned().get(key).copied()
}
}
pub struct ArcStore<T> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
pub(crate) value: Arc<RwLock<T>>,
signals: Arc<RwLock<TriggerMap>>,
keys: KeyMap,
}
impl<T> ArcStore<T> {
pub fn new(value: T) -> Self {
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
value: Arc::new(RwLock::new(value)),
signals: Default::default(),
keys: Default::default(),
}
}
}
impl<T: Default> Default for ArcStore<T> {
fn default() -> Self {
Self::new(T::default())
}
}
impl<T: Debug> Debug for ArcStore<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("ArcStore");
#[cfg(any(debug_assertions, leptos_debuginfo))]
let f = f.field("defined_at", &self.defined_at);
f.field("value", &self.value)
.field("signals", &self.signals)
.finish()
}
}
impl<T> Clone for ArcStore<T> {
fn clone(&self) -> Self {
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: self.defined_at,
value: Arc::clone(&self.value),
signals: Arc::clone(&self.signals),
keys: self.keys.clone(),
}
}
}
impl<T> DefinedAt for ArcStore<T> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
}
}
impl<T> IsDisposed for ArcStore<T> {
#[inline(always)]
fn is_disposed(&self) -> bool {
false
}
}
impl<T> ReadUntracked for ArcStore<T>
where
T: 'static,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
Plain::try_new(Arc::clone(&self.value)).map(ReadGuard::new)
}
}
impl<T> Write for ArcStore<T>
where
T: 'static,
{
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.writer()
.map(|writer| WriteGuard::new(self.clone(), writer))
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
let mut writer = self.writer()?;
writer.untrack();
Some(writer)
}
}
impl<T: 'static> Track for ArcStore<T> {
fn track(&self) {
self.track_field();
}
}
impl<T: 'static> Notify for ArcStore<T> {
fn notify(&self) {
let trigger = self.get_trigger(self.path().into_iter().collect());
trigger.this.notify();
trigger.children.notify();
}
}
pub struct Store<T, S = SyncStorage> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: &'static Location<'static>,
inner: ArenaItem<ArcStore<T>, S>,
}
impl<T> Store<T>
where
T: Send + Sync + 'static,
{
pub fn new(value: T) -> Self {
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcStore::new(value)),
}
}
}
impl<T, S> PartialEq for Store<T, S> {
fn eq(&self, other: &Self) -> bool {
self.inner == other.inner
}
}
impl<T, S> Eq for Store<T, S> {}
impl<T> Store<T, LocalStorage>
where
T: 'static,
{
pub fn new_local(value: T) -> Self {
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: Location::caller(),
inner: ArenaItem::new_with_storage(ArcStore::new(value)),
}
}
}
impl<T> Default for Store<T>
where
T: Default + Send + Sync + 'static,
{
fn default() -> Self {
Self::new(T::default())
}
}
impl<T> Default for Store<T, LocalStorage>
where
T: Default + 'static,
{
fn default() -> Self {
Self::new_local(T::default())
}
}
impl<T: Debug, S> Debug for Store<T, S>
where
S: Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("Store");
#[cfg(any(debug_assertions, leptos_debuginfo))]
let f = f.field("defined_at", &self.defined_at);
f.field("inner", &self.inner).finish()
}
}
impl<T, S> Clone for Store<T, S> {
fn clone(&self) -> Self {
*self
}
}
impl<T, S> Copy for Store<T, S> {}
impl<T, S> DefinedAt for Store<T, S> {
fn defined_at(&self) -> Option<&'static Location<'static>> {
#[cfg(any(debug_assertions, leptos_debuginfo))]
{
Some(self.defined_at)
}
#[cfg(not(any(debug_assertions, leptos_debuginfo)))]
{
None
}
}
}
impl<T, S> IsDisposed for Store<T, S>
where
T: 'static,
{
#[inline(always)]
fn is_disposed(&self) -> bool {
self.inner.is_disposed()
}
}
impl<T, S> Dispose for Store<T, S>
where
T: 'static,
{
fn dispose(self) {
self.inner.dispose();
}
}
impl<T, S> ReadUntracked for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
type Value = ReadGuard<T, Plain<T>>;
fn try_read_untracked(&self) -> Option<Self::Value> {
self.inner
.try_get_value()
.and_then(|inner| inner.try_read_untracked())
}
}
impl<T, S> Write for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
type Value = T;
fn try_write(&self) -> Option<impl UntrackableGuard<Target = Self::Value>> {
self.writer().map(|writer| WriteGuard::new(*self, writer))
}
fn try_write_untracked(
&self,
) -> Option<impl DerefMut<Target = Self::Value>> {
let mut writer = self.writer()?;
writer.untrack();
Some(writer)
}
}
impl<T, S> Track for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
fn track(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.track();
}
}
}
impl<T, S> Notify for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
fn notify(&self) {
if let Some(inner) = self.inner.try_get_value() {
inner.notify();
}
}
}
impl<T, S> From<ArcStore<T>> for Store<T, S>
where
T: 'static,
S: Storage<ArcStore<T>>,
{
fn from(value: ArcStore<T>) -> Self {
Self {
#[cfg(any(debug_assertions, leptos_debuginfo))]
defined_at: value.defined_at,
inner: ArenaItem::new_with_storage(value),
}
}
}
#[cfg(test)]
mod tests {
use crate::{self as reactive_stores, Patch, Store, StoreFieldIterator};
use reactive_graph::{
effect::Effect,
owner::StoredValue,
traits::{Read, ReadUntracked, Set, Track, Update, Write},
};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
pub async fn tick() {
tokio::time::sleep(std::time::Duration::from_micros(1)).await;
}
#[derive(Debug, Store, Patch, Default)]
struct Todos {
user: String,
todos: Vec<Todo>,
}
#[derive(Debug, Store, Patch, Default)]
struct Todo {
label: String,
completed: bool,
}
impl Todo {
pub fn new(label: impl ToString) -> Self {
Self {
label: label.to_string(),
completed: false,
}
}
}
fn data() -> Todos {
Todos {
user: "Bob".to_string(),
todos: vec![
Todo {
label: "Create reactive store".to_string(),
completed: true,
},
Todo {
label: "???".to_string(),
completed: false,
},
Todo {
label: "Profit".to_string(),
completed: false,
},
],
}
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Foo {
id: i32,
bar: Bar,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Bar {
bar_signature: i32,
baz: Baz,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baz {
more_data: i32,
baw: Baw,
}
#[derive(Debug, Clone, Store, Patch, Default)]
struct Baw {
more_data: i32,
end: i32,
}
#[tokio::test]
async fn mutating_field_triggers_effect() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
assert_eq!(store.read_untracked().todos.len(), 3);
assert_eq!(store.user().read_untracked().as_str(), "Bob");
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.user().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.user().set("Greg".into());
tick().await;
store.user().set("Carol".into());
tick().await;
store.user().update(|name| name.push_str("!!!"));
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 4);
}
#[tokio::test]
async fn other_field_does_not_notify() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.todos().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
store.user().set("Greg".into());
tick().await;
store.user().set("Carol".into());
tick().await;
store.user().update(|name| name.push_str("!!!"));
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn parent_does_notify() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.todos().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.set(Todos::default());
tick().await;
store.set(data());
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn changes_do_notify_parent() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.user().set("Greg".into());
tick().await;
store.user().set("Carol".into());
tick().await;
store.user().update(|name| name.push_str("!!!"));
tick().await;
store.todos().write().clear();
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 5);
}
#[tokio::test]
async fn iterator_tracks_the_field() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!(
"{:?}",
store.todos().iter_unkeyed().collect::<Vec<_>>()
);
combined_count.store(1, Ordering::Relaxed);
}
});
tick().await;
store
.todos()
.write()
.push(Todo::new("Create reactive store?"));
tick().await;
store.todos().write().push(Todo::new("???"));
tick().await;
store.todos().write().push(Todo::new("Profit!"));
assert_eq!(combined_count.load(Ordering::Relaxed), 1);
}
#[tokio::test]
async fn patching_only_notifies_changed_field() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(Todos {
user: "Alice".into(),
todos: vec![],
});
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.todos().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.patch(Todos {
user: "Bob".into(),
todos: vec![],
});
tick().await;
store.patch(Todos {
user: "Carol".into(),
todos: vec![],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 1);
store.patch(Todos {
user: "Carol".into(),
todos: vec![Todo {
label: "First Todo".into(),
completed: false,
}],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
}
#[tokio::test]
async fn patching_only_notifies_changed_field_with_custom_patch() {
_ = any_spawner::Executor::init_tokio();
#[derive(Debug, Store, Patch, Default)]
struct CustomTodos {
#[patch(|this, new| *this = new)]
user: String,
todos: Vec<CustomTodo>,
}
#[derive(Debug, Store, Patch, Default)]
struct CustomTodo {
label: String,
completed: bool,
}
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(CustomTodos {
user: "Alice".into(),
todos: vec![],
});
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *store.user().read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.patch(CustomTodos {
user: "Bob".into(),
todos: vec![],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 2);
store.patch(CustomTodos {
user: "Carol".into(),
todos: vec![],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
store.patch(CustomTodos {
user: "Carol".into(),
todos: vec![CustomTodo {
label: "First CustomTodo".into(),
completed: false,
}],
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn notifying_all_descendants() {
use reactive_graph::traits::*;
_ = any_spawner::Executor::init_tokio();
let store = Store::new(Foo {
id: 42,
bar: Bar {
bar_signature: 69,
baz: Baz {
more_data: 9999,
baw: Baw {
more_data: 22,
end: 1112,
},
},
},
});
let store_runs = StoredValue::new(0);
let id_runs = StoredValue::new(0);
let bar_runs = StoredValue::new(0);
let bar_signature_runs = StoredValue::new(0);
let bar_baz_runs = StoredValue::new(0);
let more_data_runs = StoredValue::new(0);
let baz_baw_end_runs = StoredValue::new(0);
Effect::new_sync(move |_| {
println!("foo: {:?}", store.get());
*store_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.id: {:?}", store.id().get());
*id_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.bar: {:?}", store.bar().get());
*bar_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.bar_signature: {:?}",
store.bar().bar_signature().get()
);
*bar_signature_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!("foo.bar.baz: {:?}", store.bar().baz().get());
*bar_baz_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.baz.more_data: {:?}",
store.bar().baz().more_data().get()
);
*more_data_runs.write_value() += 1;
});
Effect::new_sync(move |_| {
println!(
"foo.bar.baz.baw.end: {:?}",
store.bar().baz().baw().end().get()
);
*baz_baw_end_runs.write_value() += 1;
});
println!("[INITIAL EFFECT RUN]");
tick().await;
println!("\n\n[SETTING STORE]");
store.set(Default::default());
tick().await;
println!("\n\n[SETTING STORE.BAR.BAZ]");
store.bar().baz().set(Default::default());
tick().await;
assert_eq!(store_runs.get_value(), 3);
assert_eq!(id_runs.get_value(), 2);
assert_eq!(bar_runs.get_value(), 3);
assert_eq!(bar_signature_runs.get_value(), 2);
assert_eq!(bar_baz_runs.get_value(), 3);
assert_eq!(more_data_runs.get_value(), 3);
assert_eq!(baz_baw_end_runs.get_value(), 3);
}
#[tokio::test]
async fn changing_parent_notifies_subfield() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(Foo {
id: 42,
bar: Bar {
bar_signature: 69,
baz: Baz {
more_data: 9999,
baw: Baw {
more_data: 22,
end: 1112,
},
},
},
});
let tracked_field = store.bar().baz().more_data();
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *tracked_field.read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.bar().baz().set(Baz {
more_data: 42,
baw: Baw {
more_data: 11,
end: 31,
},
});
tick().await;
store.bar().set(Bar {
bar_signature: 23,
baz: Baz {
more_data: 32,
baw: Baw {
more_data: 432,
end: 423,
},
},
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn changing_parent_notifies_unkeyed_child() {
_ = any_spawner::Executor::init_tokio();
let combined_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.todos().at_unkeyed(0);
Effect::new_sync({
let combined_count = Arc::clone(&combined_count);
move |prev: Option<()>| {
if prev.is_none() {
println!("first run");
} else {
println!("next run");
}
println!("{:?}", *tracked_field.read());
combined_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
tick().await;
store.todos().write().pop();
tick().await;
store.todos().write().push(Todo {
label: "another one".into(),
completed: false,
});
tick().await;
assert_eq!(combined_count.load(Ordering::Relaxed), 3);
}
#[tokio::test]
async fn untracked_write_on_subfield_shouldnt_notify() {
_ = any_spawner::Executor::init_tokio();
let name_count = Arc::new(AtomicUsize::new(0));
let store = Store::new(data());
let tracked_field = store.user();
Effect::new_sync({
let name_count = Arc::clone(&name_count);
move |_| {
tracked_field.track();
name_count.fetch_add(1, Ordering::Relaxed);
}
});
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 1);
tracked_field.write().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
tracked_field.write_untracked().push('!');
tick().await;
assert_eq!(name_count.load(Ordering::Relaxed), 2);
}
}