Skip to main content

topcoat_runtime/
expr.rs

1use std::cell::Cell;
2
3use serde::Serialize;
4use topcoat_core::context::Cx;
5use topcoat_view::{NodeViewParts, PartsWriter};
6
7use crate::{Js, Surrogate, Surrogated};
8
9/// A value with a JavaScript expression that can produce it in the browser.
10///
11/// Expressions that read no signals render as static content. Their
12/// JavaScript remains available when needed for browser execution.
13#[derive(Debug, Clone)]
14pub struct Expr<T> {
15    pub(crate) evaluated: T,
16    pub(crate) js: Js,
17    pub(crate) is_static: bool,
18}
19
20impl<T> Expr<T> {
21    /// Evaluates an expression while observing signal reads.
22    ///
23    /// The evaluation is synchronous. Its signal reads must represent the
24    /// dependencies of the JavaScript expression as well.
25    #[inline]
26    pub fn evaluate(evaluate: impl FnOnce() -> T, js: Js) -> Self {
27        let _scope = ReadScope::enter();
28        let evaluated = evaluate();
29        Self {
30            evaluated,
31            js,
32            is_static: SIGNAL_READ.get() == Some(false),
33        }
34    }
35
36    /// Whether this expression is known not to require reactive evaluation.
37    ///
38    /// Signal reads during [`evaluate`](Self::evaluate) make it dynamic.
39    #[inline]
40    #[must_use]
41    pub fn is_static(&self) -> bool {
42        self.is_static
43    }
44
45    /// Returns the value and its JavaScript, including for static expressions.
46    #[inline]
47    pub fn into_evaluated_and_js(self) -> (T, Js) {
48        (self.evaluated, self.js)
49    }
50
51    /// Returns the captured value, carrying its dependencies into evaluation.
52    #[doc(hidden)]
53    pub fn into_captured_value(self) -> T::Surrogate
54    where
55        T: Surrogated,
56    {
57        if !self.is_static {
58            mark_signal_read();
59        }
60        self.evaluated.into_surrogate()
61    }
62}
63
64impl<T> From<T> for Expr<T>
65where
66    T: Surrogated,
67    T::Surrogate: Serialize,
68{
69    /// Creates a static expression from a value in the runtime vocabulary.
70    ///
71    /// # Panics
72    ///
73    /// Panics if the value's surrogate cannot be serialized.
74    fn from(value: T) -> Self {
75        let surrogate = value.into_surrogate();
76        let js = Js::builder().surrogate(&surrogate).build();
77        Self::evaluate(|| surrogate.into_real(), js)
78    }
79}
80
81impl<T> NodeViewParts for Expr<T>
82where
83    T: NodeViewParts,
84{
85    fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
86        if self.is_static {
87            self.evaluated.into_view_parts(cx, parts);
88            return;
89        }
90
91        // <!-- ::topcoat::expr::start("<js>") -->
92        //
93        // The comment context seals the source, so a `"` inside it renders
94        // as `&quot;` and the quotes stay unambiguous delimiters on the
95        // client.
96        parts.push_comment(|comment| {
97            comment.push_promoted_str_unescaped(&"::topcoat::expr::start(\"");
98            self.js.write(comment);
99            comment.push_promoted_str_unescaped(&"\")");
100        });
101        self.evaluated.into_view_parts(cx, parts);
102        parts.push_comment(|comment| {
103            comment.push_promoted_str_unescaped(&"::topcoat::expr::end");
104        });
105    }
106}
107
108thread_local! {
109    static SIGNAL_READ: Cell<Option<bool>> = const { Cell::new(None) };
110}
111
112/// Records a read and returns whether an expression is being evaluated.
113pub(crate) fn mark_signal_read() -> bool {
114    if SIGNAL_READ.get().is_some() {
115        SIGNAL_READ.set(Some(true));
116        true
117    } else {
118        false
119    }
120}
121
122/// Restores the enclosing synchronous evaluation, including during unwinding.
123struct ReadScope {
124    previous: Option<bool>,
125}
126
127impl ReadScope {
128    fn enter() -> Self {
129        Self {
130            previous: SIGNAL_READ.replace(Some(false)),
131        }
132    }
133}
134
135impl Drop for ReadScope {
136    fn drop(&mut self) {
137        let read = SIGNAL_READ.replace(self.previous) == Some(true);
138        if read {
139            mark_signal_read();
140        }
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use std::{panic::catch_unwind, sync::Barrier};
147
148    use super::*;
149
150    #[test]
151    fn nested_evaluations_propagate_reads_without_contaminating_siblings() {
152        let outer = Expr::evaluate(
153            || {
154                let inner = Expr::evaluate(mark_signal_read, Js::source(""));
155                assert!(!inner.is_static());
156                let sibling = Expr::evaluate(|| 1, Js::source(""));
157                assert!(sibling.is_static());
158            },
159            Js::source(""),
160        );
161        assert!(!outer.is_static());
162        assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
163    }
164
165    #[test]
166    fn panic_restores_the_parent_scope_and_preserves_its_reads() {
167        let outer = Expr::evaluate(
168            || {
169                let panic = catch_unwind(|| {
170                    Expr::evaluate(
171                        || {
172                            mark_signal_read();
173                            panic!("evaluation failed");
174                        },
175                        Js::source(""),
176                    )
177                });
178                assert!(panic.is_err());
179            },
180            Js::source(""),
181        );
182        assert!(!outer.is_static());
183        assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
184    }
185
186    #[test]
187    fn concurrent_evaluations_do_not_share_reads() {
188        let barrier = Barrier::new(2);
189        std::thread::scope(|scope| {
190            let dynamic = scope.spawn(|| {
191                Expr::evaluate(
192                    || {
193                        mark_signal_read();
194                        barrier.wait();
195                        barrier.wait();
196                    },
197                    Js::source(""),
198                )
199            });
200            let constant = Expr::evaluate(
201                || {
202                    barrier.wait();
203                    barrier.wait();
204                },
205                Js::source(""),
206            );
207            assert!(constant.is_static());
208            assert!(!dynamic.join().unwrap().is_static());
209        });
210    }
211}