1pub mod collections;
10pub mod core;
11#[macro_use]
12pub mod macros;
13pub mod primitives;
14pub mod reactivity;
15pub mod shared;
16
17pub use core::constants;
19pub use core::context::{
20 is_batching, is_tracking, is_untracking, read_version, with_context, write_version,
21 ReactiveContext,
22};
23pub use core::types::{default_equals, AnyReaction, AnySource, EqualsFn, SourceInner};
24
25pub use primitives::bind::{
27 bind, bind_chain, bind_getter, bind_readonly, bind_readonly_from, bind_readonly_static,
28 bind_static, bind_value, binding_has_internal_source, disconnect_binding, disconnect_source,
29 is_binding, unwrap_binding, unwrap_readonly, Binding, IsBinding, ReadonlyBinding,
30};
31pub use primitives::derived::{derived, derived_with_equals, Derived, DerivedInner};
32pub use primitives::effect::{
33 effect, effect_root, effect_sync, effect_sync_with_cleanup, effect_tracking,
34 effect_with_cleanup, CleanupFn, DisposeFn, Effect, EffectFn, EffectInner,
35};
36pub use primitives::linked::{
37 is_linked_signal, linked_signal, linked_signal_full, linked_signal_with_options,
38 IsLinkedSignal, LinkedSignal, LinkedSignalOptionsSimple, PreviousValue,
39};
40pub use primitives::props::{into_derived, reactive_prop, PropValue, PropsBuilder, UnwrapProp};
41pub use primitives::selector::{create_selector, create_selector_eq, Selector};
42pub use primitives::scope::{
43 effect_scope, get_current_scope, on_scope_dispose, EffectScope, ScopeCleanupFn,
44};
45pub use primitives::signal::{
46 mutable_source, signal, signal_f32, signal_f64, signal_with_equals, source, Signal,
47 SourceOptions,
48};
49pub use primitives::slot::{
50 dirty_set, is_slot, slot, slot_array, slot_with_value, tracked_slot, tracked_slot_array,
51 DirtySet, IsSlot, Slot, SlotArray, SlotWriteError, TrackedSlot, TrackedSlotArray,
52};
53
54pub use reactivity::batching::{batch, peek, tick, untrack};
56pub use reactivity::equality::{
57 always_equals, by_field, deep_equals, equals, never_equals, safe_equals_f32, safe_equals_f64,
58 safe_equals_option_f64, safe_not_equal_f32, safe_not_equal_f64, shallow_equals_slice,
59 shallow_equals_vec,
60};
61pub use reactivity::scheduling::flush_sync;
62pub use reactivity::tracking::{
63 is_dirty, mark_reactions, notify_write, remove_reactions, set_signal_status, track_read,
64};
65
66pub use collections::{ReactiveMap, ReactiveSet, ReactiveVec};
68
69pub use primitives::repeater::{repeat, RepeaterInner};
71
72pub use shared::{
74 wait_for_wake, wait_for_wake_timeout, MutableSharedArray, MutableSharedF32Array,
75 ReactiveSharedArray, ReactiveSharedF32Array, ReactiveSharedI32Array, ReactiveSharedU32Array,
76 ReactiveSharedU8Array, SharedBufferContext,
77};
78
79pub use shared::notify::{platform_wake, AtomicsNotifier, Notifier, NoopNotifier};
81pub use shared::shared_slot_buffer::SharedSlotBuffer;
82
83#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::rc::Rc;
91
92 #[test]
97 fn phase1_success_criteria_1_flags_defined() {
98 assert_eq!(constants::SOURCE, 1 << 0);
100 assert_eq!(constants::DERIVED, 1 << 1);
101 assert_eq!(constants::EFFECT, 1 << 2);
102
103 assert_eq!(constants::CLEAN, 1 << 10);
105 assert_eq!(constants::DIRTY, 1 << 11);
106 assert_eq!(constants::MAYBE_DIRTY, 1 << 12);
107
108 assert_eq!(constants::CLEAN & constants::DIRTY, 0);
110 assert_eq!(constants::DIRTY & constants::MAYBE_DIRTY, 0);
111 }
112
113 #[test]
114 fn phase1_success_criteria_2_traits_compile() {
115 let source: Rc<SourceInner<i32>> = Rc::new(SourceInner::new(42));
116 let _any_source: Rc<dyn AnySource> = source;
117
118 let source = SourceInner::new(100);
119 assert!(source.flags() & constants::SOURCE != 0);
120 source.mark_dirty();
121 assert!(source.is_dirty());
122 }
123
124 #[test]
125 fn phase1_success_criteria_3_thread_local_context() {
126 with_context(|ctx| {
127 assert_eq!(ctx.get_write_version(), 1);
128 assert!(!ctx.has_active_reaction());
129
130 ctx.increment_write_version();
131 assert_eq!(ctx.get_write_version(), 2);
132 });
133
134 assert!(write_version() >= 1);
135 assert!(!is_tracking());
136 }
137
138 #[test]
139 fn phase1_success_criteria_4_heterogeneous_storage() {
140 let int_source: Rc<dyn AnySource> = Rc::new(SourceInner::new(42i32));
141 let string_source: Rc<dyn AnySource> = Rc::new(SourceInner::new(String::from("hello")));
142 let float_source: Rc<dyn AnySource> = Rc::new(SourceInner::new(3.14f64));
143 let bool_source: Rc<dyn AnySource> = Rc::new(SourceInner::new(true));
144 let vec_source: Rc<dyn AnySource> = Rc::new(SourceInner::new(vec![1, 2, 3]));
145
146 let sources: Vec<Rc<dyn AnySource>> = vec![
147 int_source,
148 string_source,
149 float_source,
150 bool_source,
151 vec_source,
152 ];
153
154 assert_eq!(sources.len(), 5);
155
156 for source in &sources {
157 assert!(source.flags() & constants::SOURCE != 0);
158 assert!(source.is_clean());
159 }
160
161 sources[0].mark_dirty();
162 sources[2].mark_maybe_dirty();
163
164 assert!(sources[0].is_dirty());
165 assert!(sources[1].is_clean());
166 assert!(sources[2].is_maybe_dirty());
167 }
168
169 #[test]
174 fn phase2_success_criteria_1_signal_api() {
175 let count = signal(0);
177
178 assert_eq!(count.get(), 0);
180
181 count.set(42);
183 assert_eq!(count.get(), 42);
184 }
185
186 #[test]
187 fn phase2_success_criteria_2_heterogeneous_signal_storage() {
188 let int_signal = signal(42i32);
190 let string_signal = signal(String::from("hello"));
191
192 let sources: Vec<Rc<dyn AnySource>> = vec![
193 int_signal.as_any_source(),
194 string_signal.as_any_source(),
195 ];
196
197 assert_eq!(sources.len(), 2);
198
199 for source in &sources {
201 assert!(source.flags() & constants::SOURCE != 0);
202 }
203 }
204
205 #[test]
206 fn phase2_success_criteria_3_combinators() {
207 let items = signal(vec![1, 2, 3, 4, 5]);
208
209 assert_eq!(items.try_get(), Some(vec![1, 2, 3, 4, 5]));
211
212 let sum = items.with(|v| v.iter().sum::<i32>());
214 assert_eq!(sum, 15);
215
216 items.update(|v| v.push(6));
218 assert_eq!(items.get(), vec![1, 2, 3, 4, 5, 6]);
219 }
220
221 #[test]
222 fn phase2_success_criteria_4_equality_checking() {
223 let count = signal(42);
224
225 let changed = count.set(42);
227 assert!(!changed);
228
229 let changed = count.set(100);
231 assert!(changed);
232 }
233
234 use std::any::Any;
239 use std::cell::{Cell, RefCell};
240
241 struct TestReaction {
243 flags: Cell<u32>,
244 deps: RefCell<Vec<Rc<dyn AnySource>>>,
245 }
246
247 impl TestReaction {
248 fn new() -> Rc<Self> {
249 Rc::new(Self {
250 flags: Cell::new(constants::EFFECT | constants::CLEAN),
251 deps: RefCell::new(Vec::new()),
252 })
253 }
254 }
255
256 impl AnyReaction for TestReaction {
257 fn flags(&self) -> u32 {
258 self.flags.get()
259 }
260
261 fn set_flags(&self, flags: u32) {
262 self.flags.set(flags);
263 }
264
265 fn dep_count(&self) -> usize {
266 self.deps.borrow().len()
267 }
268
269 fn add_dep(&self, source: Rc<dyn AnySource>) {
270 self.deps.borrow_mut().push(source);
271 }
272
273 fn clear_deps(&self) {
274 self.deps.borrow_mut().clear();
275 }
276
277 fn remove_deps_from(&self, start: usize) {
278 self.deps.borrow_mut().truncate(start);
279 }
280
281 fn for_each_dep(&self, f: &mut dyn FnMut(&Rc<dyn AnySource>) -> bool) {
282 for dep in self.deps.borrow().iter() {
283 if !f(dep) {
284 break;
285 }
286 }
287 }
288
289 fn remove_source(&self, source: &Rc<dyn AnySource>) {
290 let source_ptr = Rc::as_ptr(source) as *const ();
291 self.deps.borrow_mut().retain(|dep| {
292 let dep_ptr = Rc::as_ptr(dep) as *const ();
293 dep_ptr != source_ptr
294 });
295 }
296
297 fn update(&self) -> bool {
298 false
299 }
300
301 fn as_any(&self) -> &dyn Any {
302 self
303 }
304
305 fn as_derived_source(&self) -> Option<Rc<dyn AnySource>> {
306 None
307 }
308 }
309
310 #[test]
311 fn phase3_success_criteria_1_read_registers_dependency() {
312 let count = signal(42);
314 let reaction = TestReaction::new();
315
316 with_context(|ctx| {
318 ctx.set_active_reaction(Some(Rc::downgrade(
319 &(reaction.clone() as Rc<dyn AnyReaction>),
320 )));
321 });
322
323 let value = count.get();
325 assert_eq!(value, 42);
326
327 with_context(|ctx| {
329 ctx.set_active_reaction(None);
330 });
331
332 assert_eq!(reaction.dep_count(), 1);
334
335 assert_eq!(count.inner().reaction_count(), 1);
337 }
338
339 #[test]
340 fn phase3_success_criteria_2_write_marks_reactions_dirty() {
341 let count = signal(0);
343 let reaction = TestReaction::new();
344
345 count.inner().add_reaction(Rc::downgrade(
347 &(reaction.clone() as Rc<dyn AnyReaction>),
348 ));
349
350 assert!(reaction.is_clean());
352 assert!(!reaction.is_dirty());
353
354 count.set(42);
356
357 assert!(reaction.is_dirty());
359 assert!(!reaction.is_clean());
360 }
361
362 #[test]
363 fn phase3_success_criteria_3_is_dirty_reports_correctly() {
364 let reaction = TestReaction::new();
366
367 assert!(!is_dirty(&*reaction));
369
370 reaction.mark_dirty();
372 assert!(is_dirty(&*reaction));
373
374 reaction.mark_maybe_dirty();
376 assert!(is_dirty(&*reaction));
377
378 reaction.mark_clean();
380 assert!(!is_dirty(&*reaction));
381 }
382
383 #[test]
384 fn phase3_success_criteria_4_remove_reactions_cleanup() {
385 let source1 = signal(1);
387 let source2 = signal(2);
388 let source3 = signal(3);
389 let reaction = TestReaction::new();
390
391 reaction.add_dep(source1.as_any_source());
393 reaction.add_dep(source2.as_any_source());
394 reaction.add_dep(source3.as_any_source());
395
396 source1
398 .inner()
399 .add_reaction(Rc::downgrade(&(reaction.clone() as Rc<dyn AnyReaction>)));
400 source2
401 .inner()
402 .add_reaction(Rc::downgrade(&(reaction.clone() as Rc<dyn AnyReaction>)));
403 source3
404 .inner()
405 .add_reaction(Rc::downgrade(&(reaction.clone() as Rc<dyn AnyReaction>)));
406
407 assert_eq!(reaction.dep_count(), 3);
408
409 remove_reactions(reaction.clone(), 1);
411
412 assert_eq!(reaction.dep_count(), 1);
414
415 assert_eq!(source2.inner().reaction_count(), 0);
417 assert_eq!(source3.inner().reaction_count(), 0);
418
419 assert_eq!(source1.inner().reaction_count(), 1);
421 }
422
423 #[test]
424 fn phase3_success_criteria_5_no_borrow_panics_cascade() {
425 let source = signal(0);
429
430 let reactions: Vec<Rc<TestReaction>> = (0..10).map(|_| TestReaction::new()).collect();
432
433 for reaction in &reactions {
435 source.inner().add_reaction(Rc::downgrade(
436 &(reaction.clone() as Rc<dyn AnyReaction>),
437 ));
438 }
439
440 source.set(42);
442
443 for reaction in &reactions {
445 assert!(
446 reaction.is_dirty(),
447 "All reactions should be marked dirty after signal write"
448 );
449 }
450 }
451
452 #[test]
453 fn phase3_integration_full_cycle() {
454 let a = signal(1);
457 let b = signal(2);
458 let reaction = TestReaction::new();
459
460 with_context(|ctx| {
462 ctx.set_active_reaction(Some(Rc::downgrade(
463 &(reaction.clone() as Rc<dyn AnyReaction>),
464 )));
465 });
466
467 let sum = a.get() + b.get();
469 assert_eq!(sum, 3);
470
471 with_context(|ctx| {
473 ctx.set_active_reaction(None);
474 });
475
476 assert_eq!(reaction.dep_count(), 2);
478 assert_eq!(a.inner().reaction_count(), 1);
479 assert_eq!(b.inner().reaction_count(), 1);
480
481 reaction.mark_clean();
483 assert!(reaction.is_clean());
484
485 a.set(10);
487 assert!(reaction.is_dirty());
488
489 reaction.mark_clean();
491 b.set(20);
492 assert!(reaction.is_dirty());
493 }
494
495 #[test]
500 fn phase4_success_criteria_1_derived_api() {
501 let count = signal(1);
503 let doubled = derived({
504 let count = count.clone();
505 move || count.get() * 2
506 });
507
508 assert_eq!(doubled.get(), 2);
509
510 count.set(5);
511 assert_eq!(doubled.get(), 10);
512 }
513
514 #[test]
515 fn phase4_success_criteria_2_caches_and_recomputes() {
516 let compute_count = Rc::new(Cell::new(0));
518
519 let a = signal(1);
520 let d = derived({
521 let a = a.clone();
522 let compute_count = compute_count.clone();
523 move || {
524 compute_count.set(compute_count.get() + 1);
525 a.get() * 2
526 }
527 });
528
529 assert_eq!(d.get(), 2);
531 assert_eq!(compute_count.get(), 1);
532
533 assert_eq!(d.get(), 2);
535 assert_eq!(compute_count.get(), 1);
536
537 a.set(5);
539 assert_eq!(d.get(), 10);
540 assert_eq!(compute_count.get(), 2);
541
542 assert_eq!(d.get(), 10);
544 assert_eq!(compute_count.get(), 2);
545 }
546
547 #[test]
548 fn phase4_success_criteria_3_maybe_dirty_optimization() {
549 let compute_c_count = Rc::new(Cell::new(0));
554
555 let a = signal(0);
556
557 let b = derived({
559 let a = a.clone();
560 move || a.get().clamp(0, 10)
561 });
562
563 let c = derived({
564 let b = b.clone();
565 let compute_c_count = compute_c_count.clone();
566 move || {
567 compute_c_count.set(compute_c_count.get() + 1);
568 b.get() * 100
569 }
570 });
571
572 assert_eq!(c.get(), 0); assert_eq!(compute_c_count.get(), 1);
575
576 a.set(0);
578 assert_eq!(c.get(), 0);
579 a.set(5);
584 assert_eq!(c.get(), 500);
585 }
586
587 #[test]
588 fn phase4_success_criteria_4_diamond_dependency() {
589 let a = signal(1);
598
599 let b = derived({
600 let a = a.clone();
601 move || a.get() + 10
602 });
603
604 let c = derived({
605 let a = a.clone();
606 move || a.get() * 10
607 });
608
609 let d = derived({
610 let b = b.clone();
611 let c = c.clone();
612 move || b.get() + c.get()
613 });
614
615 assert_eq!(d.get(), 21);
617
618 a.set(2);
620 assert_eq!(d.get(), 32);
621
622 }
624
625 #[test]
626 fn phase4_success_criteria_5_cascade_propagation() {
627 let a = signal(1);
631
632 let b = derived({
633 let a = a.clone();
634 move || a.get() * 2
635 });
636
637 let c = derived({
638 let b = b.clone();
639 move || b.get() + 10
640 });
641
642 assert_eq!(c.get(), 12);
644
645 let b_inner = b.inner();
647 let c_inner = c.inner();
648
649 assert!(AnySource::is_clean(&**b_inner));
651 assert!(AnySource::is_clean(&**c_inner));
652
653 a.set(5);
655
656 let b_flags = AnySource::flags(&**b_inner);
659 let c_flags = AnySource::flags(&**c_inner);
660
661 assert!(
662 (b_flags & constants::DIRTY) != 0,
663 "B should be marked DIRTY"
664 );
665 assert!(
666 (c_flags & (constants::DIRTY | constants::MAYBE_DIRTY)) != 0,
667 "C should be marked DIRTY or MAYBE_DIRTY via cascade"
668 );
669
670 assert_eq!(c.get(), 20); assert!(AnySource::is_clean(&**b_inner));
675 assert!(AnySource::is_clean(&**c_inner));
676 }
677}