Skip to main content

threadloom_core/
lib.rs

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