Skip to main content

threadloom_core/
lib.rs

1#![allow(warnings)]
2use std::cell::RefCell;
3use std::collections::{HashMap, HashSet};
4use std::rc::Rc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7pub use serde_json;
8
9static NEXT_RUNTIME_ID: AtomicUsize = AtomicUsize::new(1);
10
11thread_local! {
12    static RUNTIME_ID: usize = NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed);
13    static GRAPH: RefCell<Graph> = RefCell::new(Graph::new());
14}
15
16#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
17pub struct NodeId {
18    runtime_id: usize,
19    index: usize,
20}
21
22#[derive(Clone, Copy, PartialEq, Eq, Debug)]
23enum State {
24    Clean,
25    Check,
26    Dirty,
27}
28
29type ComputeFn = Rc<RefCell<dyn FnMut() -> bool>>;
30
31struct Node {
32    id: NodeId,
33    state: State,
34    sources: HashMap<NodeId, usize>,
35    subscribers: HashSet<NodeId>,
36    compute: Option<ComputeFn>,
37    is_effect: bool,
38    version: usize,
39    value: Option<Box<dyn std::any::Any>>,
40}
41
42struct Graph {
43    nodes: Vec<Option<Node>>,
44    next_id: usize,
45    current_subscriber: Option<NodeId>,
46    pending_effects: HashSet<NodeId>,
47    /// INVARIANT: This queue must only ever hold `NodeId` values (which are `Copy` and `Send`),
48    /// never the `Boundary` object itself.
49    ///
50    /// `Boundary` contains an `Rc` and cannot cross thread boundaries. A coordinator will drain these
51    /// `NodeId`s, route them to the correct worker thread based on their `runtime_id`, and that
52    /// thread will look up its local `HashMap<NodeId, Boundary>` to re-evaluate the boundary.
53    pub pending_boundaries: HashSet<NodeId>,
54    is_batching: bool,
55}
56
57impl Graph {
58    fn new() -> Self {
59        Self {
60            nodes: Vec::new(),
61            next_id: 0,
62            current_subscriber: None,
63            pending_effects: HashSet::new(),
64            pending_boundaries: HashSet::new(),
65            is_batching: false,
66        }
67    }
68}
69
70impl NodeId {
71    pub fn runtime_id(&self) -> usize {
72        self.runtime_id
73    }
74
75    pub fn index(&self) -> usize {
76        self.index
77    }
78    
79    pub fn test_new(runtime_id: usize, index: usize) -> Self {
80        Self { runtime_id, index }
81    }
82
83    pub fn new_empty() -> Self {
84        Self::new(false, None, None)
85    }
86
87    fn set_compute(&self, compute: ComputeFn) {
88        GRAPH.with(|g| {
89            let mut g = g.borrow_mut();
90            if let Some(node) = &mut g.nodes[self.index] {
91                node.compute = Some(compute);
92            }
93        });
94    }
95
96    fn new(is_effect: bool, compute: Option<ComputeFn>, value: Option<Box<dyn std::any::Any>>) -> Self {
97        GRAPH.with(|g| {
98            let mut g = g.borrow_mut();
99            let index = g.next_id;
100            let runtime_id = RUNTIME_ID.with(|id| *id);
101            let id = NodeId { runtime_id, index };
102            g.next_id += 1;
103            g.nodes.push(Some(Node {
104                id,
105                state: State::Clean,
106                sources: HashMap::new(),
107                subscribers: HashSet::new(),
108                compute,
109                is_effect,
110                version: 0,
111                value,
112            }));
113            id
114        })
115    }
116
117    fn record_read(&self) {
118        let current_runtime = RUNTIME_ID.with(|id| *id);
119        if self.runtime_id != current_runtime {
120            // TODO: Cross-shard / global state currently fails fast here. We need a real resolution
121            // path for this before Phase 3's scheduler is done, so unrelated subtrees can share state
122            // (e.g. a Redux-like store) without just crashing.
123            panic!(
124                "Cross-shard signal read detected! This will be handled by explicit synchronization in Phase 3."
125            );
126        }
127        GRAPH.with(|g| {
128            let mut g = g.borrow_mut();
129            if let Some(sub_id) = g.current_subscriber {
130                println!(
131                    "record_read: {} is subscribing to {}",
132                    sub_id.index, self.index
133                );
134                if let Some(node) = &mut g.nodes[self.index] {
135                    node.subscribers.insert(sub_id);
136                }
137                let version = g.nodes[self.index].as_ref().unwrap().version;
138                if let Some(sub_node) = &mut g.nodes[sub_id.index] {
139                    sub_node.sources.insert(*self, version);
140                }
141            }
142        });
143    }
144
145    fn mark_dirty(&self) {
146        println!("mark_dirty called on {}", self.index);
147        let mut stack = vec![*self];
148        while let Some(current) = stack.pop() {
149            let (is_effect, subscribers, should_push, has_compute) = GRAPH.with(|g| {
150                let mut g = g.borrow_mut();
151                if let Some(node) = g.nodes[current.index].as_mut() {
152                    let state = node.state;
153                    let is_effect = node.is_effect;
154                    let subs = node.subscribers.iter().copied().collect::<Vec<_>>();
155
156                    if current == *self || state == State::Clean {
157                        if current != *self {
158                            node.state = State::Check;
159                        } else {
160                            node.state = State::Dirty;
161                        }
162                        println!("node {} is now {:?} (is_effect: {}, has_compute: {})", current.index, node.state, is_effect, node.compute.is_some());
163                        (is_effect, subs, true, node.compute.is_some())
164                    } else {
165                        (false, vec![], false, false)
166                    }
167                } else {
168                    (false, vec![], false, false)
169                }
170            });
171
172            if should_push {
173                if is_effect {
174                    if has_compute {
175                        GRAPH.with(|g| g.borrow_mut().pending_effects.insert(current));
176                    } else {
177                        GRAPH.with(|g| g.borrow_mut().pending_boundaries.insert(current));
178                    }
179                }
180                for sub in subscribers {
181                    stack.push(sub);
182                }
183            }
184        }
185    }
186
187    fn clear_sources(&self) {
188        GRAPH.with(|g| {
189            let mut g = g.borrow_mut();
190            let sources = g.nodes[self.index].as_ref().unwrap().sources.clone();
191            for (source, _) in sources {
192                if let Some(s) = &mut g.nodes[source.index] {
193                    s.subscribers.remove(self);
194                }
195            }
196            if let Some(node) = &mut g.nodes[self.index] {
197                node.sources.clear();
198            }
199        });
200    }
201
202    fn update_if_necessary(&self) -> usize {
203        let state = GRAPH.with(|g| g.borrow().nodes[self.index].as_ref().unwrap().state);
204        if state == State::Clean {
205            return GRAPH.with(|g| g.borrow().nodes[self.index].as_ref().unwrap().version);
206        }
207        if state == State::Check {
208            let sources = GRAPH.with(|g| {
209                g.borrow().nodes[self.index]
210                    .as_ref()
211                    .unwrap()
212                    .sources
213                    .clone()
214            });
215            for (source, old_version) in sources {
216                let new_version = source.update_if_necessary();
217                if new_version > old_version {
218                    GRAPH.with(|g| {
219                        g.borrow_mut().nodes[self.index].as_mut().unwrap().state = State::Dirty
220                    });
221                    break;
222                }
223            }
224        }
225
226        let state = GRAPH.with(|g| g.borrow().nodes[self.index].as_ref().unwrap().state);
227
228        if state == State::Dirty {
229            let compute = GRAPH.with(|g| {
230                g.borrow().nodes[self.index]
231                    .as_ref()
232                    .unwrap()
233                    .compute
234                    .clone()
235            });
236            if let Some(compute) = compute {
237                self.clear_sources();
238                let prev_sub = GRAPH.with(|g| {
239                    let mut g = g.borrow_mut();
240                    let prev = g.current_subscriber;
241                    g.current_subscriber = Some(*self);
242                    prev
243                });
244
245                let changed = {
246                    let mut c = compute.borrow_mut();
247                    c()
248                };
249
250                GRAPH.with(|g| {
251                    let mut g = g.borrow_mut();
252                    g.current_subscriber = prev_sub;
253                    let node = g.nodes[self.index].as_mut().unwrap();
254                    node.state = State::Clean;
255                    if changed {
256                        node.version += 1;
257                    }
258                });
259            } else {
260                GRAPH.with(|g| {
261                    let mut g = g.borrow_mut();
262                    let node = g.nodes[self.index].as_mut().unwrap();
263                    node.state = State::Clean;
264                    node.version += 1;
265                });
266            }
267        } else {
268            GRAPH.with(|g| g.borrow_mut().nodes[self.index].as_mut().unwrap().state = State::Clean);
269        }
270
271        GRAPH.with(|g| g.borrow().nodes[self.index].as_ref().unwrap().version)
272    }
273
274    pub fn track<R>(&self, f: impl FnOnce() -> R) -> R {
275        let prev_sub = GRAPH.with(|g| {
276            let mut g = g.borrow_mut();
277            let prev = g.current_subscriber;
278            g.current_subscriber = Some(*self);
279
280            // Clear old sources before re-tracking
281            let sources = g.nodes[self.index].as_ref().unwrap().sources.clone();
282            for (source, _) in sources {
283                if let Some(s) = &mut g.nodes[source.index] {
284                    s.subscribers.remove(self);
285                }
286            }
287            if let Some(node) = &mut g.nodes[self.index] {
288                node.sources.clear();
289            }
290
291            prev
292        });
293
294        let result = f();
295
296        GRAPH.with(|g| {
297            let mut g = g.borrow_mut();
298            g.current_subscriber = prev_sub;
299            g.nodes[self.index].as_mut().unwrap().state = State::Clean;
300        });
301
302        result
303    }
304
305    pub fn is_dirty(&self) -> bool {
306        GRAPH.with(|g| {
307            let state = g.borrow().nodes[self.index].as_ref().unwrap().state;
308            state == State::Dirty || state == State::Check
309        })
310    }
311}
312
313pub fn take_pending_boundaries() -> Vec<NodeId> {
314    GRAPH.with(|g| {
315        let mut g = g.borrow_mut();
316        let boundaries: Vec<_> = g.pending_boundaries.iter().copied().collect();
317        g.pending_boundaries.clear();
318        boundaries
319    })
320}
321
322pub fn run_effects() {
323    let is_batching = GRAPH.with(|g| g.borrow().is_batching);
324    if is_batching {
325        return;
326    }
327    GRAPH.with(|g| g.borrow_mut().is_batching = true);
328
329    loop {
330        let effects: Vec<NodeId> = GRAPH.with(|g| {
331            let mut g = g.borrow_mut();
332            let effects: Vec<_> = g.pending_effects.iter().copied().collect();
333            g.pending_effects.clear();
334            effects
335        });
336
337        if effects.is_empty() {
338            break;
339        }
340
341        for effect in effects {
342            effect.update_if_necessary();
343        }
344    }
345
346    GRAPH.with(|g| g.borrow_mut().is_batching = false);
347}
348
349// ---------------------------------------------------------
350// PUBLIC API
351// ---------------------------------------------------------
352
353pub struct ReadSignal<T> {
354    id: NodeId,
355    _marker: std::marker::PhantomData<T>,
356}
357
358impl<T> Copy for ReadSignal<T> {}
359impl<T> Clone for ReadSignal<T> {
360    fn clone(&self) -> Self {
361        *self
362    }
363}
364
365pub struct WriteSignal<T> {
366    id: NodeId,
367    _marker: std::marker::PhantomData<T>,
368}
369
370impl<T> Copy for WriteSignal<T> {}
371impl<T> Clone for WriteSignal<T> {
372    fn clone(&self) -> Self {
373        *self
374    }
375}
376
377pub fn create_signal<T: Clone + 'static>(initial: T) -> (ReadSignal<T>, WriteSignal<T>) {
378    let id = NodeId::new(false, None, Some(Box::new(initial)));
379    (
380        ReadSignal {
381            id,
382            _marker: std::marker::PhantomData,
383        },
384        WriteSignal {
385            id,
386            _marker: std::marker::PhantomData,
387        },
388    )
389}
390
391impl<T: Clone + 'static> ReadSignal<T> {
392    pub fn get(&self) -> T {
393        self.id.record_read();
394        GRAPH.with(|g| {
395            let g = g.borrow();
396            let node = g.nodes[self.id.index].as_ref().unwrap();
397            node.value.as_ref().unwrap().downcast_ref::<T>().unwrap().clone()
398        })
399    }
400}
401
402impl<T: Clone + PartialEq + 'static> WriteSignal<T> {
403    pub fn set(&self, new_value: T) {
404        let changed = GRAPH.with(|g| {
405            let mut g = g.borrow_mut();
406            let node = g.nodes[self.id.index].as_mut().unwrap();
407            let val = node.value.as_mut().unwrap().downcast_mut::<T>().unwrap();
408            if *val == new_value {
409                false
410            } else {
411                *val = new_value;
412                true
413            }
414        });
415        if changed {
416            self.id.mark_dirty();
417            run_effects();
418        }
419    }
420}
421
422pub fn create_effect<F>(mut f: F)
423where
424    F: FnMut() + 'static,
425{
426    let compute: ComputeFn = Rc::new(RefCell::new(move || {
427        f();
428        true
429    }));
430
431    let id = NodeId::new(true, Some(compute.clone()), None);
432
433    id.mark_dirty();
434    run_effects();
435}
436
437pub struct Memo<T> {
438    id: NodeId,
439    _marker: std::marker::PhantomData<T>,
440}
441
442impl<T> Copy for Memo<T> {}
443impl<T> Clone for Memo<T> {
444    fn clone(&self) -> Self {
445        *self
446    }
447}
448
449pub fn create_memo<T, F>(mut f: F) -> Memo<T>
450where
451    F: FnMut() -> T + 'static,
452    T: Clone + PartialEq + 'static,
453{
454    let id = NodeId::new_empty();
455
456    GRAPH.with(|g| {
457        g.borrow_mut().nodes[id.index].as_mut().unwrap().value = Some(Box::new(None::<T>));
458    });
459
460    let compute: ComputeFn = Rc::new(RefCell::new(move || {
461        let new_value = f();
462        let changed = GRAPH.with(|g| {
463            let mut g = g.borrow_mut();
464            let node = g.nodes[id.index].as_mut().unwrap();
465            let val_any = node.value.as_mut().unwrap();
466            let val = val_any.downcast_mut::<Option<T>>().unwrap();
467            match val {
468                Some(old_value) if *old_value == new_value => false,
469                _ => {
470                    *val = Some(new_value);
471                    true
472                }
473            }
474        });
475        changed
476    }));
477
478    id.set_compute(compute);
479    id.mark_dirty();
480    id.update_if_necessary();
481
482    Memo { id, _marker: std::marker::PhantomData }
483}
484
485impl<T: Clone + 'static> Memo<T> {
486    pub fn get(&self) -> T {
487        self.id.update_if_necessary();
488        self.id.record_read();
489        GRAPH.with(|g| {
490            let g = g.borrow();
491            let node = g.nodes[self.id.index].as_ref().unwrap();
492            node.value.as_ref().unwrap().downcast_ref::<Option<T>>().unwrap().clone().unwrap()
493        })
494    }
495}
496
497// ---------------------------------------------------------
498// COMPONENT MODEL / VIEW BUILDER
499// ---------------------------------------------------------
500
501#[derive(Clone)]
502pub enum AttributeValue {
503    String(String),
504    Bool(bool),
505    Dynamic(Rc<dyn Fn() -> AttributeValue>),
506    Event(Rc<dyn Fn()>),
507}
508
509impl std::fmt::Debug for AttributeValue {
510    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511        match self {
512            Self::String(s) => write!(f, "String({:?})", s),
513            Self::Bool(b) => write!(f, "Bool({})", b),
514            Self::Dynamic(_) => write!(f, "Dynamic(..)"),
515            Self::Event(_) => write!(f, "Event(..)"),
516        }
517    }
518}
519
520impl From<&str> for AttributeValue {
521    fn from(s: &str) -> Self {
522        AttributeValue::String(s.to_string())
523    }
524}
525impl From<String> for AttributeValue {
526    fn from(s: String) -> Self {
527        AttributeValue::String(s)
528    }
529}
530impl From<bool> for AttributeValue {
531    fn from(b: bool) -> Self {
532        AttributeValue::Bool(b)
533    }
534}
535impl<F: Fn() -> String + 'static> From<F> for AttributeValue {
536    fn from(f: F) -> Self {
537        AttributeValue::Dynamic(Rc::new(move || AttributeValue::String(f())))
538    }
539}
540impl From<Rc<dyn Fn() -> AttributeValue>> for AttributeValue {
541    fn from(f: Rc<dyn Fn() -> AttributeValue>) -> Self {
542        AttributeValue::Dynamic(f)
543    }
544}
545
546/// Represents a dynamic UI boundary.
547/// 
548/// ```compile_fail
549/// use std::sync::mpsc;
550/// use std::rc::Rc;
551/// use std::cell::RefCell;
552/// use threadloom_core::{Boundary, NodeId, View};
553/// 
554/// // This test proves that Boundary cannot cross threads!
555/// // If someone tries to send a Boundary over a channel, it will fail to compile
556/// // because Boundary contains an Rc.
557/// let (tx, rx) = mpsc::channel::<Boundary>();
558/// std::thread::spawn(move || {
559///     // tx is moved into the thread, requiring T (Boundary) to be Send
560/// });
561/// ```
562#[derive(Clone)]
563pub struct Boundary {
564    pub id: NodeId,
565    pub compute: Rc<RefCell<dyn FnMut() -> View>>,
566}
567
568impl std::fmt::Debug for Boundary {
569    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
570        write!(
571            f,
572            "Boundary(runtime_id: {}, index: {})",
573            self.id.runtime_id, self.id.index
574        )
575    }
576}
577
578#[derive(Clone)]
579pub enum View {
580    Text(String),
581    DynamicNode(Boundary),
582    Element {
583        tag: String,
584        attrs: std::collections::HashMap<String, AttributeValue>,
585        children: Vec<View>,
586    },
587    Fragment(Vec<View>),
588    None,
589}
590
591impl std::fmt::Debug for View {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        match self {
594            Self::Text(s) => write!(f, "Text({:?})", s),
595            Self::DynamicNode(_) => write!(f, "DynamicNode(..)"),
596            Self::Element {
597                tag,
598                attrs,
599                children,
600            } => f
601                .debug_struct("Element")
602                .field("tag", tag)
603                .field("attrs", attrs)
604                .field("children", children)
605                .finish(),
606            Self::Fragment(c) => write!(f, "Fragment({:?})", c),
607            Self::None => write!(f, "None"),
608        }
609    }
610}
611
612impl View {
613    pub fn with_attr(mut self, key: &str, value: &str) -> Self {
614        match &mut self {
615            View::Element { attrs, .. } => {
616                attrs.insert(key.to_string(), crate::AttributeValue::String(value.to_string()));
617            }
618            View::Fragment(children) => {
619                if let Some(first) = children.first_mut() {
620                    *first = std::mem::replace(first, View::None).with_attr(key, value);
621                }
622            }
623            _ => {}
624        }
625        self
626    }
627}
628
629pub trait IntoView {
630    fn into_view(self) -> View;
631}
632
633impl IntoView for String {
634    fn into_view(self) -> View {
635        View::Text(self)
636    }
637}
638impl IntoView for &str {
639    fn into_view(self) -> View {
640        View::Text(self.to_string())
641    }
642}
643impl IntoView for View {
644    fn into_view(self) -> View {
645        self
646    }
647}
648impl<T: IntoView> IntoView for Vec<T> {
649    fn into_view(self) -> View {
650        View::Fragment(self.into_iter().map(|c| c.into_view()).collect())
651    }
652}
653impl<T: IntoView> IntoView for Option<T> {
654    fn into_view(self) -> View {
655        self.map(|t| t.into_view()).unwrap_or(View::None)
656    }
657}
658
659macro_rules! impl_into_view_for_display {
660    ($($t:ty),*) => {
661        $(
662            impl IntoView for $t {
663                fn into_view(self) -> View {
664                    View::Text(self.to_string())
665                }
666            }
667        )*
668    }
669}
670impl_into_view_for_display!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize, f32, f64, bool);
671
672impl<T: IntoView + 'static, F: FnMut() -> T + 'static> IntoView for F {
673    fn into_view(mut self) -> View {
674        let id = NodeId::new(true, None, None);
675        View::DynamicNode(Boundary {
676            id,
677            compute: Rc::new(RefCell::new(move || self().into_view())),
678        })
679    }
680}
681
682// Builders
683pub struct ElementBuilder {
684    tag: String,
685    attrs: std::collections::HashMap<String, AttributeValue>,
686    children: Vec<View>,
687}
688
689impl ElementBuilder {
690    pub fn new(tag: impl Into<String>) -> Self {
691        Self {
692            tag: tag.into(),
693            attrs: std::collections::HashMap::new(),
694            children: vec![],
695        }
696    }
697    pub fn attr(mut self, key: impl Into<String>, value: impl Into<AttributeValue>) -> Self {
698        self.attrs.insert(key.into(), value.into());
699        self
700    }
701    pub fn on(mut self, key: impl Into<String>, f: impl Fn() + 'static) -> Self {
702        self.attrs
703            .insert(key.into(), AttributeValue::Event(Rc::new(f)));
704        self
705    }
706    pub fn child(mut self, child: impl IntoView) -> Self {
707        self.children.push(child.into_view());
708        self
709    }
710}
711
712impl IntoView for ElementBuilder {
713    fn into_view(self) -> View {
714        View::Element {
715            tag: self.tag,
716            attrs: self.attrs,
717            children: self.children,
718        }
719    }
720}
721
722pub fn element(tag: impl Into<String>) -> ElementBuilder {
723    ElementBuilder::new(tag)
724}
725pub fn text(text: impl Into<String>) -> View {
726    View::Text(text.into())
727}
728pub fn dyn_node<F: FnMut() -> View + 'static>(f: F) -> View {
729    f.into_view()
730}
731pub fn fragment(children: impl IntoIterator<Item = View>) -> View {
732    View::Fragment(children.into_iter().collect())
733}
734
735#[macro_export]
736macro_rules! create_store {
737    ($vis:vis $name:ident, $type:ty, $init:expr) => {
738        $vis struct $name;
739        impl $name {
740            fn store() -> ($crate::ReadSignal<$type>, $crate::WriteSignal<$type>) {
741                thread_local! {
742                    static STORE: ($crate::ReadSignal<$type>, $crate::WriteSignal<$type>) = $crate::create_signal($init);
743                }
744                STORE.with(|s| *s)
745            }
746            $vis fn get() -> $type {
747                Self::store().0.get()
748            }
749            $vis fn set(val: $type) {
750                Self::store().1.set(val)
751            }
752            $vis fn update(f: impl FnOnce(&mut $type)) {
753                let mut val = Self::get();
754                f(&mut val);
755                Self::set(val);
756            }
757        }
758    };
759}
760
761// ---------------------------------------------------------
762// NEW FEATURES
763// ---------------------------------------------------------
764
765pub struct Signal;
766impl Signal {
767    pub fn computed<T, F>(f: F) -> Memo<T>
768    where
769        F: FnMut() -> T + 'static,
770        T: Clone + PartialEq + 'static,
771    {
772        create_memo(f)
773    }
774}
775
776pub struct GlobalSignal<T: 'static> {
777    init: fn() -> T,
778}
779impl<T: Clone + PartialEq + 'static> GlobalSignal<T> {
780    pub const fn new(init: fn() -> T) -> Self {
781        Self { init }
782    }
783    
784    fn get_signals(&self) -> (ReadSignal<T>, WriteSignal<T>) {
785        thread_local! {
786            static GLOBALS: RefCell<HashMap<usize, (NodeId, NodeId)>> = RefCell::new(HashMap::new());
787        }
788        let addr = self as *const _ as usize;
789        GLOBALS.with(|g| {
790            let mut g = g.borrow_mut();
791            if let Some(&(r, w)) = g.get(&addr) {
792                (ReadSignal { id: r, _marker: std::marker::PhantomData }, WriteSignal { id: w, _marker: std::marker::PhantomData })
793            } else {
794                let (read, write) = create_signal((self.init)());
795                g.insert(addr, (read.id, write.id));
796                (read, write)
797            }
798        })
799    }
800
801    pub fn get(&self) -> T {
802        self.get_signals().0.get()
803    }
804    
805    pub fn set(&self, value: T) {
806        self.get_signals().1.set(value)
807    }
808
809    pub fn update(&self, f: impl FnOnce(&mut T)) {
810        let mut val = self.get();
811        f(&mut val);
812        self.set(val);
813    }
814}
815
816pub struct Action<I, O> {
817    is_loading: ReadSignal<bool>,
818    set_loading: WriteSignal<bool>,
819    func: std::rc::Rc<dyn Fn(I) -> std::pin::Pin<Box<dyn std::future::Future<Output = O>>>>,
820}
821
822impl<I: 'static, O: 'static> Clone for Action<I, O> {
823    fn clone(&self) -> Self {
824        Self {
825            is_loading: self.is_loading,
826            set_loading: self.set_loading,
827            func: self.func.clone(),
828        }
829    }
830}
831
832impl<I: 'static, O: 'static> Action<I, O> {
833    pub fn new<F, Fut>(f: F) -> Self 
834    where 
835        F: Fn(I) -> Fut + 'static,
836        Fut: std::future::Future<Output = O> + 'static
837    {
838        let (is_loading, set_loading) = create_signal(false);
839        let func = std::rc::Rc::new(move |i| Box::pin(f(i)) as std::pin::Pin<Box<dyn std::future::Future<Output = O>>>);
840        Self { is_loading, set_loading, func }
841    }
842    
843    pub fn is_loading(&self) -> bool {
844        self.is_loading.get()
845    }
846    
847    pub async fn execute(&self, input: I) -> O {
848        self.set_loading.set(true);
849        let res = (self.func)(input).await;
850        self.set_loading.set(false);
851        res
852    }
853}
854
855// ---------------------------------------------------------
856// HYDRATION (Feature 5)
857// ---------------------------------------------------------
858pub fn serialize_signal_graph() -> String {
859    // In a real Zero-JS scenario, we'd walk GRAPH and serialize nodes to JSON.
860    // For now, return empty state.
861    "{}".to_string()
862}
863
864pub fn hydrate_signal_graph(_json: &str) {
865    // Restore signal state from serialized graph.
866}
867
868#[cfg(test)]
869mod tests {
870    use super::*;
871    use std::cell::RefCell;
872    use std::rc::Rc;
873
874    #[test]
875    fn test_diamond_problem() {
876        let (read_sig, write_sig) = create_signal(1);
877
878        let a_run_count = Rc::new(RefCell::new(0));
879        let b_run_count = Rc::new(RefCell::new(0));
880        let c_run_count = Rc::new(RefCell::new(0));
881
882        let arc = a_run_count.clone();
883        let read_a = read_sig.clone();
884        let memo_a = create_memo(move || {
885            *arc.borrow_mut() += 1;
886            read_a.get() * 2
887        });
888
889        let brc = b_run_count.clone();
890        let read_b = read_sig.clone();
891        let memo_b = create_memo(move || {
892            *brc.borrow_mut() += 1;
893            read_b.get() * 3
894        });
895
896        let crc = c_run_count.clone();
897        let memo_a_c = memo_a.clone();
898        let memo_b_c = memo_b.clone();
899        create_effect(move || {
900            *crc.borrow_mut() += 1;
901            let _: i32 = memo_a_c.get() + memo_b_c.get();
902        });
903
904        assert_eq!(*a_run_count.borrow(), 1);
905        assert_eq!(*b_run_count.borrow(), 1);
906        assert_eq!(*c_run_count.borrow(), 1);
907
908        write_sig.set(2);
909
910        assert_eq!(*a_run_count.borrow(), 2);
911        assert_eq!(*b_run_count.borrow(), 2);
912        assert_eq!(*c_run_count.borrow(), 2); // Effect runs EXACTLY ONCE for diamond!
913    }
914
915    #[test]
916    fn test_conditional_subscription() {
917        let (read_a, write_a) = create_signal(true);
918        let (read_b, write_b) = create_signal(10);
919
920        let run_count = Rc::new(RefCell::new(0));
921        let rc = run_count.clone();
922        let read_a_c = read_a.clone();
923        let read_b_c = read_b.clone();
924
925        create_effect(move || {
926            *rc.borrow_mut() += 1;
927            if read_a_c.get() {
928                let _: i32 = read_b_c.get();
929            }
930        });
931
932        assert_eq!(*run_count.borrow(), 1);
933
934        // changing B triggers effect when A is true
935        write_b.set(20);
936        assert_eq!(*run_count.borrow(), 2);
937
938        // turning A off triggers effect (reads A)
939        write_a.set(false);
940        assert_eq!(*run_count.borrow(), 3);
941
942        // changing B now should NOT trigger effect because it's unsubscribed
943        write_b.set(30);
944        assert_eq!(*run_count.borrow(), 3); // Does not run!
945
946        // turning A back on triggers effect, and resubscribes to B
947        write_a.set(true);
948        assert_eq!(*run_count.borrow(), 4);
949
950        // changing B now triggers effect again
951        write_b.set(40);
952        assert_eq!(*run_count.borrow(), 5);
953    }
954
955    #[test]
956    fn test_thread_safety_invariants() {
957        fn assert_send<T: Send>() {}
958        
959        // Prove that NodeId can safely cross thread boundaries
960        assert_send::<NodeId>();
961    }
962}
963
964#[cfg(target_arch = "wasm32")]
965pub async fn client_rpc_call<T: serde::de::DeserializeOwned>(url: &str, body: serde_json::Value) -> Result<T, String> {
966    use wasm_bindgen::JsCast;
967
968    let mut opts = web_sys::RequestInit::new();
969    opts.method("POST");
970    opts.mode(web_sys::RequestMode::Cors);
971
972    let js_body = wasm_bindgen::JsValue::from_str(&body.to_string());
973    opts.body(Some(&js_body));
974
975    let headers = web_sys::Headers::new()
976        .map_err(|e| format!("Headers::new failed: {:?}", e))?;
977    headers.set("Content-Type", "application/json")
978        .map_err(|e| format!("set Content-Type failed: {:?}", e))?;
979    headers.set("x-threadloom-route", url)
980        .map_err(|e| format!("set x-threadloom-route failed: {:?}", e))?;
981    opts.headers(&headers);
982
983    let request = web_sys::Request::new_with_str_and_init(url, &opts)
984        .map_err(|e| format!("Request::new failed: {:?}", e))?;
985
986    let window = web_sys::window().ok_or_else(|| "no window".to_string())?;
987    let resp_value = wasm_bindgen_futures::JsFuture::from(window.fetch_with_request(&request))
988        .await
989        .map_err(|e| format!("fetch failed: {:?}", e))?;
990    let resp: web_sys::Response = resp_value
991        .dyn_into()
992        .map_err(|e| format!("cast to Response failed: {:?}", e))?;
993
994    if !resp.ok() {
995        let status = resp.status();
996        let err_text = match resp.text() {
997            Ok(promise) => {
998                match wasm_bindgen_futures::JsFuture::from(promise).await {
999                    Ok(js_val) => js_val.as_string().unwrap_or_default(),
1000                    Err(_) => "Could not read response text".to_string()
1001                }
1002            },
1003            Err(_) => "Could not read response text promise".to_string()
1004        };
1005        return Err(format!("server returned HTTP {}: {}", status, err_text));
1006    }
1007
1008    let text_promise = resp.text().map_err(|e| format!("resp.text() failed: {:?}", e))?;
1009    let text_val = wasm_bindgen_futures::JsFuture::from(text_promise)
1010        .await
1011        .map_err(|e| format!("reading body failed: {:?}", e))?;
1012    let text = text_val.as_string().ok_or_else(|| "body is not a string".to_string())?;
1013
1014    serde_json::from_str(&text).map_err(|e| format!("deserialize failed: {} | body was: {}", e, text))
1015}