Skip to main content

threadloom_core/
lib.rs

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