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