1use std::{
2 cell::{Cell, RefCell},
3 collections::VecDeque,
4 rc::{Rc, Weak},
5};
6
7mod graph;
8mod memo;
9pub mod versions;
10use graph::{Observer, ObserverKind, Source, track};
11pub use memo::{Memo, memo, memo_with_eq};
12
13thread_local! {
14 static CURRENT: RefCell<Option<Weak<Observer>>> = const { RefCell::new(None) };
15 static QUEUE: RefCell<VecDeque<Weak<EffectInner>>> = const { RefCell::new(VecDeque::new()) };
16 static BATCH_DEPTH: Cell<usize> = const { Cell::new(0) };
17 static FLUSHING: Cell<bool> = const { Cell::new(false) };
18 static NEXT_ID: Cell<u64> = const { Cell::new(0) };
19 static NEXT_FLUSH: Cell<u64> = const { Cell::new(0) };
20 static NEXT_WAVE: Cell<u64> = const { Cell::new(0) };
21 static COMPUTING: Cell<usize> = const { Cell::new(0) };
22 static RENDERING: Cell<bool> = const { Cell::new(false) };
23 #[cfg(feature = "javascript")]
24 static AFTER_FLUSH: RefCell<VecDeque<Box<dyn FnOnce()>>> = const { RefCell::new(VecDeque::new()) };
25}
26
27struct RenderGuard(bool);
28
29impl RenderGuard {
30 fn replace(rendering: bool) -> Self {
31 Self(RENDERING.with(|current| current.replace(rendering)))
32 }
33}
34
35impl Drop for RenderGuard {
36 fn drop(&mut self) {
37 RENDERING.with(|current| current.set(self.0));
38 }
39}
40
41fn assert_not_computing() {
42 assert_eq!(
43 COMPUTING.with(Cell::get),
44 0,
45 "memo computations and equality functions must not write signals or create effects"
46 );
47}
48
49struct SignalInner<T> {
50 value: RefCell<T>,
51 source: Rc<Source>,
52 render_values: RefCell<Vec<(Rc<T>, versions::Versions)>>,
53}
54
55pub struct Signal<T>(Rc<SignalInner<T>>);
57
58impl<T> Clone for Signal<T> {
59 fn clone(&self) -> Self {
60 Self(self.0.clone())
61 }
62}
63
64pub fn signal<T>(value: T) -> Signal<T> {
66 Signal(Rc::new(SignalInner {
67 value: RefCell::new(value),
68 source: Rc::new(Source::new(None)),
69 render_values: RefCell::new(Vec::new()),
70 }))
71}
72
73impl<T> Signal<T> {
74 pub fn with<R>(&self, read: impl FnOnce(&T) -> R) -> R {
77 if let Some((value, inputs)) = self.render_value() {
78 versions::Versions::exclude(|| track(&self.0.source));
79 inputs.include();
80 return read(&value);
81 }
82 track(&self.0.source);
83 read(&self.0.value.borrow())
84 }
85
86 pub fn with_untracked<R>(&self, read: impl FnOnce(&T) -> R) -> R {
88 if let Some((value, _)) = self.render_value() {
89 return read(&value);
90 }
91 read(&self.0.value.borrow())
92 }
93
94 #[doc(hidden)]
104 pub fn with_render_value<R>(
105 &self,
106 value: Rc<T>,
107 inputs: versions::Versions,
108 render: impl FnOnce() -> R,
109 ) -> R {
110 struct Pop<'a, T>(&'a Signal<T>);
111 impl<T> Drop for Pop<'_, T> {
112 fn drop(&mut self) {
113 let value = self.0.0.render_values.borrow_mut().pop();
114 drop(value);
115 }
116 }
117 self.0.render_values.borrow_mut().push((value, inputs));
118 let _rendering = RenderGuard::replace(true);
119 let _pop = Pop(self);
120 render()
121 }
122
123 pub fn update<R>(&self, update: impl FnOnce(&mut T) -> R) -> R {
127 assert_not_computing();
128 crate::coherence::mutation("signal write");
129 let result = update(&mut self.0.value.borrow_mut());
130 notify(&self.0.source);
131 result
132 }
133
134 pub fn replace(&self, value: T) -> T {
138 self.update(|current| std::mem::replace(current, value))
139 }
140
141 fn render_value(&self) -> Option<(Rc<T>, versions::Versions)> {
142 RENDERING
143 .with(Cell::get)
144 .then(|| self.0.render_values.borrow().last().cloned())
145 .flatten()
146 }
147}
148
149impl<T: Clone> Signal<T> {
150 pub fn get(&self) -> T {
152 self.with(Clone::clone)
153 }
154
155 pub fn get_untracked(&self) -> T {
157 self.with_untracked(Clone::clone)
158 }
159}
160
161impl<T: PartialEq> Signal<T> {
162 pub fn set(&self, value: T) {
166 assert_not_computing();
167 crate::coherence::mutation("signal write");
168 let retired = {
169 let mut current = self.0.value.borrow_mut();
170 if *current == value {
171 None
172 } else {
173 Some(std::mem::replace(&mut *current, value))
174 }
175 };
176 if retired.is_some() {
177 notify(&self.0.source);
178 }
179 drop(retired);
180 }
181}
182
183pub struct Derived<T>(Rc<dyn Fn() -> T>);
186
187impl<T> Clone for Derived<T> {
188 fn clone(&self) -> Self {
189 Self(self.0.clone())
190 }
191}
192
193pub fn derived<T>(compute: impl Fn() -> T + 'static) -> Derived<T> {
194 Derived(Rc::new(compute))
195}
196
197impl<T> Derived<T> {
198 pub fn get(&self) -> T {
199 (self.0)()
200 }
201}
202
203struct EffectInner {
204 observer: Rc<Observer>,
205 active: Cell<bool>,
206 queued: Cell<bool>,
207 last_flush: Cell<u64>,
208 flush_runs: Cell<u32>,
209 callback: RefCell<Box<dyn FnMut()>>,
210 lifecycle: RefCell<Vec<crate::Registration>>,
211}
212
213impl EffectInner {
214 fn unsubscribe(&self) {
215 self.observer.unsubscribe();
216 }
217
218 fn run(self: &Rc<Self>, initial: bool) {
219 if !self.active.get() {
220 return;
221 }
222 if !initial && !untrack(|| self.observer.changed()) {
225 return;
226 }
227 let _run = self.observer.begin();
229 let _tracking = TrackingGuard::replace(Some(Rc::downgrade(&self.observer)));
230 (self.callback.borrow_mut())();
231 }
232}
233
234#[must_use = "retain the effect handle for as long as the subscription should live"]
236pub struct Effect(Rc<EffectInner>);
237
238impl Effect {
239 #[cfg(feature = "dom")]
240 pub(crate) fn initializer(&self) -> impl FnOnce() + 'static {
241 let weak = Rc::downgrade(&self.0);
242 move || {
243 if let Some(inner) = weak.upgrade() {
244 batch(|| inner.run(true));
245 }
246 }
247 }
248
249 pub fn dispose(&self) {
251 self.0.active.set(false);
252 self.0.unsubscribe();
253 }
254}
255
256impl Drop for Effect {
257 fn drop(&mut self) {
258 self.dispose();
259 }
260}
261
262fn allocate_effect(callback: impl FnMut() + 'static) -> Effect {
265 assert_not_computing();
266 Effect(Rc::new_cyclic(|weak| EffectInner {
267 observer: Observer::new(ObserverKind::Effect(weak.clone())),
268 active: Cell::new(true),
269 queued: Cell::new(false),
270 last_flush: Cell::new(0),
271 flush_runs: Cell::new(0),
272 callback: RefCell::new(Box::new(callback)),
273 lifecycle: RefCell::new(Vec::new()),
274 }))
275}
276
277#[cfg(feature = "dom")]
278pub(crate) fn prepared_effect(callback: impl FnMut() + 'static) -> Effect {
279 allocate_effect(callback)
280}
281
282pub fn effect(callback: impl FnMut() + 'static) -> Effect {
283 let subscription = allocate_effect(callback);
284 if let Some(owner) = crate::coherence::preparing_owner().filter(|owner| !owner.is_active()) {
287 let weak = Rc::downgrade(&subscription.0);
288 let activation = owner.on_activate(move || {
289 if let Some(inner) = weak.upgrade() {
290 batch(|| inner.run(true));
291 }
292 });
293 let weak = Rc::downgrade(&subscription.0);
294 let cleanup = owner.on_cleanup(move || {
295 if let Some(inner) = weak.upgrade() {
296 inner.active.set(false);
297 inner.unsubscribe();
298 }
299 });
300 subscription
301 .0
302 .lifecycle
303 .borrow_mut()
304 .extend([activation, cleanup]);
305 } else if !crate::coherence::mutation("effect creation") {
306 batch(|| subscription.0.run(true));
307 }
308 subscription
309}
310
311fn notify(source: &Source) {
312 source.advance();
313 source.notify();
315 flush();
316}
317
318fn clear_queue() {
319 #[cfg(feature = "javascript")]
320 AFTER_FLUSH.with(|queue| queue.borrow_mut().clear());
321 QUEUE.with(|queue| {
322 for pending in queue
323 .borrow_mut()
324 .drain(..)
325 .filter_map(|item| item.upgrade())
326 {
327 pending.queued.set(false);
328 }
329 });
330}
331
332struct FlushGuard;
333
334impl Drop for FlushGuard {
335 fn drop(&mut self) {
336 FLUSHING.with(|flushing| flushing.set(false));
337 if std::thread::panicking() {
338 clear_queue();
339 }
340 }
341}
342
343fn flush() {
344 if BATCH_DEPTH.with(Cell::get) > 0 || FLUSHING.with(|flushing| flushing.replace(true)) {
345 return;
346 }
347 let _guard = FlushGuard;
348 let epoch = NEXT_FLUSH.with(|next| {
349 let epoch = next
350 .get()
351 .checked_add(1)
352 .expect("reactive flush ID exhausted");
353 next.set(epoch);
354 epoch
355 });
356 loop {
357 let next = QUEUE.with(|queue| queue.borrow_mut().pop_front());
358 let Some(next) = next else {
359 #[cfg(feature = "javascript")]
360 {
361 let callback = AFTER_FLUSH.with(|queue| queue.borrow_mut().pop_front());
362 if let Some(callback) = callback {
363 untrack(callback);
364 continue;
365 }
366 }
367 break;
368 };
369 if let Some(next) = next.upgrade() {
370 next.queued.set(false);
371 let runs = if next.last_flush.replace(epoch) == epoch {
374 next.flush_runs.get() + 1
375 } else {
376 1
377 };
378 next.flush_runs.set(runs);
379 assert!(
380 runs <= 10_000,
381 "reactive cycle: one effect exceeded 10,000 runs in one flush"
382 );
383 next.run(false);
384 }
385 }
386}
387
388#[cfg(feature = "javascript")]
392pub(crate) fn after_flush(callback: impl FnOnce() + 'static) {
393 AFTER_FLUSH.with(|queue| queue.borrow_mut().push_back(Box::new(callback)));
394}
395
396struct TrackingGuard(Option<Weak<Observer>>);
397
398impl TrackingGuard {
399 fn replace(next: Option<Weak<Observer>>) -> Self {
400 Self(CURRENT.with(|current| current.replace(next)))
401 }
402}
403
404impl Drop for TrackingGuard {
405 fn drop(&mut self) {
406 CURRENT.with(|current| current.replace(self.0.take()));
407 }
408}
409
410pub fn untrack<R>(read: impl FnOnce() -> R) -> R {
412 let _guard = TrackingGuard::replace(None);
413 read()
414}
415
416struct BatchGuard;
417
418impl Drop for BatchGuard {
419 fn drop(&mut self) {
420 BATCH_DEPTH.with(|depth| depth.set(depth.get() - 1));
421 if std::thread::panicking() {
422 clear_queue();
423 } else {
424 flush();
425 }
426 }
427}
428
429pub fn batch<R>(update: impl FnOnce() -> R) -> R {
432 BATCH_DEPTH.with(|depth| depth.set(depth.get() + 1));
433 let _guard = BatchGuard;
434 update()
435}