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