1use std::{any::TypeId, collections::HashMap, panic::Location, sync::Arc};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
4use topcoat_core::{
5 context::{Cx, identity, try_request_context},
6 identity::{Identity, SiteKey},
7};
8use topcoat_view::{HoistKey, hoist, hoist_once};
9
10use crate::{Surrogate, Surrogated};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct SignalId(u128);
18
19impl SignalId {
20 pub(crate) fn derive(identity: Identity, location: &Location<'_>) -> Self {
22 Self(identity.child(SiteKey::from_location(location)).hash())
23 }
24}
25
26impl std::fmt::Display for SignalId {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 write!(f, "{:032x}", self.0)
29 }
30}
31
32impl Serialize for SignalId {
33 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
34 serializer.collect_str(self)
35 }
36}
37
38impl<'de> Deserialize<'de> for SignalId {
39 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
40 struct Visitor;
41
42 impl serde::de::Visitor<'_> for Visitor {
43 type Value = SignalId;
44
45 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 f.write_str("a hex signal id")
47 }
48
49 fn visit_str<E: serde::de::Error>(self, hex: &str) -> Result<Self::Value, E> {
50 u128::from_str_radix(hex, 16)
51 .map(SignalId)
52 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(hex), &self))
53 }
54 }
55
56 deserializer.deserialize_str(Visitor)
57 }
58}
59
60#[derive(Debug, Default, Deserialize)]
66#[serde(transparent)]
67pub struct SignalValues(HashMap<SignalId, serde_json::Value>);
68
69impl SignalValues {
70 fn get(&self, id: SignalId) -> Option<&serde_json::Value> {
72 self.0.get(&id)
73 }
74}
75
76#[derive(Debug)]
89pub struct Signal<T> {
90 id: SignalId,
91 value: Arc<T>,
93}
94
95impl<T> Signal<T> {
96 #[inline]
97 pub(crate) fn new(id: SignalId, value: T) -> Self {
98 Self {
99 id,
100 value: Arc::new(value),
101 }
102 }
103
104 pub(crate) fn id(&self) -> SignalId {
105 self.id
106 }
107
108 #[must_use]
118 #[track_caller]
119 pub fn read(&self) -> &T {
120 self.track();
121 &self.value
122 }
123
124 #[must_use]
129 pub fn read_untracked(&self) -> &T {
130 crate::expr::mark_signal_read();
131 &self.value
132 }
133
134 #[track_caller]
137 fn track(&self) {
138 if crate::expr::mark_signal_read() {
140 return;
141 }
142 let id = self.id;
143 hoist_once(
144 HoistKey::new((TypeId::of::<SignalId>(), id)),
145 move |parts| {
146 parts.push_comment(|comment| {
147 comment
148 .push_promoted_str_unescaped(&"::topcoat::dep(\"")
149 .push_string_unescaped(id.to_string())
150 .push_promoted_str_unescaped(&"\")");
151 });
152 },
153 );
154 }
155
156 fn declaration(&self) -> String
159 where
160 T: SignalValue,
161 {
162 #[derive(Serialize)]
163 struct Declaration<'a, V>
164 where
165 V: ?Sized,
166 {
167 t: &'static str,
168 id: SignalId,
169 v: &'a V,
170 }
171
172 let value = self.value.surrogate();
173 let declaration = Declaration {
174 t: "signal",
175 id: self.id,
176 v: &value,
177 };
178 serde_json::to_string(&declaration).expect("failed to serialize signal declaration")
179 }
180}
181
182impl<T> Signal<T>
183where
184 T: Clone,
185{
186 #[must_use]
195 #[track_caller]
196 pub fn get(&self) -> T {
197 self.track();
198 T::clone(&self.value)
199 }
200
201 #[must_use]
205 pub fn get_untracked(&self) -> T {
206 T::clone(self.read_untracked())
207 }
208}
209
210impl<T> Clone for Signal<T> {
211 fn clone(&self) -> Self {
212 Self {
213 id: self.id,
214 value: Arc::clone(&self.value),
215 }
216 }
217}
218
219pub trait SignalValue: Sized {
224 type Surrogate<'a>: Serialize
226 where
227 Self: 'a;
228
229 fn surrogate(&self) -> Self::Surrogate<'_>;
231
232 fn from_value(value: &serde_json::Value) -> Option<Self>;
235}
236
237impl<T> SignalValue for T
238where
239 T: Surrogated,
240 T::Surrogate: DeserializeOwned,
241 for<'a> &'a T: Surrogated,
242 for<'a> <&'a T as Surrogated>::Surrogate: Serialize,
243{
244 type Surrogate<'a>
245 = <&'a T as Surrogated>::Surrogate
246 where
247 Self: 'a;
248
249 fn surrogate(&self) -> Self::Surrogate<'_> {
250 self.into_surrogate()
251 }
252
253 fn from_value(value: &serde_json::Value) -> Option<Self> {
254 T::Surrogate::deserialize(value)
255 .ok()
256 .map(Surrogate::into_real)
257 }
258}
259
260#[track_caller]
300pub fn signal<T>(cx: &Cx, init: impl FnOnce() -> T) -> Signal<T>
301where
302 T: SignalValue,
303{
304 let id = SignalId::derive(identity(cx), Location::caller());
305 let value = try_request_context::<SignalValues>(cx)
306 .and_then(|values| values.get(id))
307 .and_then(T::from_value)
308 .unwrap_or_else(init);
309 let signal = Signal::new(id, value);
310 let declaration = signal.declaration();
311 hoist(move |parts| {
312 parts.push_comment(|comment| {
313 comment
316 .push_promoted_str_unescaped(&"::topcoat::signal(")
317 .push_string(declaration)
318 .push_promoted_str_unescaped(&")");
319 });
320 });
321 signal
322}
323
324#[cfg(test)]
325mod tests {
326 use std::{
327 panic::{AssertUnwindSafe, catch_unwind},
328 pin::pin,
329 sync::{
330 Arc, OnceLock,
331 atomic::{AtomicBool, Ordering},
332 },
333 task::{Context, Poll, Waker},
334 };
335
336 use topcoat::view::{HoistView, ViewExt, internal::ThenView, view};
337 use topcoat_core::context::with_identity;
338
339 use super::*;
340
341 const SITE_A: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
342 const SITE_B: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
343
344 fn block_on<F: Future>(future: F) -> F::Output {
346 let mut future = pin!(future);
347 let mut cx = Context::from_waker(Waker::noop());
348 loop {
349 if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
350 return output;
351 }
352 }
353 }
354
355 fn render_with_signal(value: &'static str) -> String {
358 let cx = &Cx::default();
359 let view = HoistView::new(ThenView::new(async move {
360 let signal = signal(cx, || String::from(value));
361 Ok(view! { cx => <p>(signal.read_untracked())</p> })
362 }));
363 block_on(view.single()).unwrap().render(cx)
364 }
365
366 fn render_reading(read: impl Fn(&Signal<String>) + Send + 'static) -> (String, String) {
369 let cx = &Cx::default();
370 let id = Arc::new(OnceLock::new());
371 let out = Arc::clone(&id);
372 let view = HoistView::new(ThenView::new(async move {
373 let signal = signal(cx, || String::from("x"));
374 read(&signal);
375 out.set(signal.id()).unwrap();
376 Ok(view! { cx => <p></p> })
377 }));
378 let html = block_on(view.single()).unwrap().render(cx);
379 let marker = format!("<!--::topcoat::dep(\"{}\")-->", id.get().unwrap());
380 (html, marker)
381 }
382
383 fn signal_id_at(site: SiteKey) -> SignalId {
386 let cx = &with_identity(Cx::default(), Identity::ROOT.child(site));
387 let view = HoistView::new(ThenView::new(async move {
388 let signal = signal(cx, || 0.0_f64);
389 Ok(view! { cx => <p>(signal.id().to_string())</p> })
390 }));
391 let html = block_on(view.single()).unwrap().render(cx);
392 let start = html.find("<p>").unwrap() + 3;
393 let end = html.rfind("</p>").unwrap();
394 SignalId(u128::from_str_radix(&html[start..end], 16).unwrap())
395 }
396
397 #[test]
398 fn creating_a_signal_outside_a_body_panics() {
399 let cx = Cx::default();
400 let panic = catch_unwind(AssertUnwindSafe(|| signal(&cx, || 0.0_f64))).unwrap_err();
401 let message = panic.downcast::<&str>().expect("panics with a message");
402 assert!(message.contains("no view is collecting hoisted parts"));
403 }
404
405 #[test]
406 fn the_same_call_site_renders_the_same_id_every_time() {
407 assert_eq!(render_with_signal("x"), render_with_signal("x"));
408 assert_eq!(signal_id_at(SITE_A), signal_id_at(SITE_A));
409 }
410
411 #[test]
412 fn distinct_call_sites_render_distinct_ids() {
413 let cx = &Cx::default();
414 let view = HoistView::new(ThenView::new(async move {
415 let first = signal(cx, || 0.0_f64);
416 let second = signal(cx, || 0.0_f64);
417 assert_ne!(first.id(), second.id());
418 Ok(view! { cx => <p></p> })
419 }));
420 block_on(view.single()).unwrap();
421 }
422
423 #[test]
424 fn distinct_identities_render_distinct_ids() {
425 assert_ne!(signal_id_at(SITE_A), signal_id_at(SITE_B));
426 }
427
428 #[test]
429 fn an_ambiguous_identity_panics() {
430 let cx = with_identity(
431 Cx::default(),
432 Identity::ROOT.ambiguous_child(SITE_A, "`card` at src/a.rs:1"),
433 );
434 let panic = catch_unwind(AssertUnwindSafe(|| signal(&cx, || 0.0_f64))).unwrap_err();
435 let message = panic.downcast::<String>().expect("panics with a message");
436 assert!(message.contains("`card` at src/a.rs:1"), "{message}");
437 }
438
439 fn number_signal(cx: &Cx) -> (SignalId, f64, bool) {
442 let seen = Arc::new(OnceLock::new());
443 let out = Arc::clone(&seen);
444 let view = HoistView::new(ThenView::new(async move {
445 let init_ran = AtomicBool::new(false);
446 let signal = signal(cx, || {
447 init_ran.store(true, Ordering::Relaxed);
448 1.0_f64
449 });
450 out.set((
451 signal.id(),
452 *signal.read_untracked(),
453 init_ran.load(Ordering::Relaxed),
454 ))
455 .unwrap();
456 Ok(view! { cx => <p></p> })
457 }));
458 block_on(view.single()).unwrap();
459 *seen.get().unwrap()
460 }
461
462 fn cx_carrying(id: SignalId, value: serde_json::Value) -> Cx {
464 Cx::default().with(SignalValues(HashMap::from([(id, value)])))
465 }
466
467 #[test]
468 fn a_signal_resumes_from_the_value_the_request_carries() {
469 let (id, value, init_ran) = number_signal(&Cx::default());
470 assert_eq!((value, init_ran), (1.0, true));
471
472 let cx = cx_carrying(id, serde_json::json!(5.0));
473 assert_eq!(number_signal(&cx), (id, 5.0, false));
474 }
475
476 #[test]
477 fn context_keys_distinguish_repeated_helper_calls() {
478 fn keyed_number(key: u32) -> SignalId {
479 number_signal(&Cx::default().keyed(key)).0
480 }
481
482 let first = keyed_number(1);
483 let second = keyed_number(2);
484 assert_ne!(first, second);
485 assert_eq!(keyed_number(2), second);
486 assert_eq!(keyed_number(1), first);
487
488 let cx = Cx::default();
489 let first = number_signal(&cx.keyed(())).0;
490 let second = number_signal(&cx.keyed(())).0;
491 assert_ne!(first, second);
492 }
493
494 #[test]
495 #[should_panic(expected = "identity cannot be read inside memoized functions")]
496 fn memoized_functions_cannot_create_signals() {
497 let cx = Cx::default();
498 topcoat_core::context::memoize_cache(&cx)
499 .memoize(&cx, (), (), |cx, ()| signal(&cx.keyed(()), || 0.0));
500 }
501
502 #[test]
503 fn a_value_that_does_not_fit_the_signal_is_ignored() {
504 let (id, ..) = number_signal(&Cx::default());
505
506 let cx = cx_carrying(id, serde_json::json!("five"));
507 assert_eq!(number_signal(&cx), (id, 1.0, true));
508 }
509
510 #[test]
511 fn an_id_round_trips_through_json_as_hex() {
512 let id = SignalId(0x1234_abcd);
513 let json = serde_json::to_string(&id).unwrap();
514 assert_eq!(json, format!("\"{:032x}\"", 0x1234_abcd_u128));
515 assert_eq!(serde_json::from_str::<SignalId>(&json).unwrap(), id);
516 assert!(serde_json::from_str::<SignalId>("\"zz\"").is_err());
517 }
518
519 #[test]
520 fn the_declaration_renders_ahead_of_the_content() {
521 let html = render_with_signal("x");
522 assert!(html.starts_with("<!--::topcoat::signal("), "{html}");
523 assert!(html.ends_with("--><p>x</p>"), "{html}");
524 }
525
526 #[test]
527 fn payload_cannot_terminate_the_comment() {
528 let html = render_with_signal("a-->b\"c&d");
531
532 assert_eq!(html.matches("-->").count(), 1, "{html}");
535 assert!(html.contains("-->"), "{html}");
536 assert!(html.contains("""), "{html}");
538 }
539
540 #[test]
541 fn a_tracked_read_renders_a_dependency_marker_after_the_declaration() {
542 let (html, marker) = render_reading(|signal| {
543 assert_eq!(signal.get(), "x");
544 });
545 assert!(html.starts_with("<!--::topcoat::signal("), "{html}");
546 assert!(html.ends_with(&format!("{marker}<p></p>")), "{html}");
547 }
548
549 #[test]
550 fn repeated_reads_render_one_dependency_marker() {
551 let (html, marker) = render_reading(|signal| {
552 let _ = signal.get();
553 let _ = signal.read();
554 let _ = signal.get();
555 });
556 assert_eq!(html.matches(&marker).count(), 1, "{html}");
557 }
558
559 #[test]
560 fn untracked_reads_render_no_dependency_marker() {
561 let (html, _) = render_reading(|signal| {
562 assert_eq!(signal.get_untracked(), "x");
563 assert_eq!(signal.read_untracked(), "x");
564 });
565 assert!(!html.contains("::topcoat::dep("), "{html}");
566 }
567
568 #[test]
569 fn a_read_in_a_runtime_expression_renders_no_dependency_marker() {
570 let (html, _) = render_reading(|signal| {
571 let surrogate = crate::SignalSurrogate::new(signal.clone());
572 let _ = surrogate.get();
573 let _ = surrogate.read();
574 });
575 assert!(!html.contains("::topcoat::dep("), "{html}");
576 }
577
578 #[test]
579 fn a_tracked_read_outside_a_body_panics() {
580 let signal = Signal::new(SignalId(1), String::from("x"));
581 let panic = catch_unwind(AssertUnwindSafe(|| signal.get())).unwrap_err();
582 let message = panic.downcast::<&str>().expect("panics with a message");
583 assert!(message.contains("no view is collecting hoisted parts"));
584 }
585
586 #[test]
587 fn untracked_reads_work_outside_a_body() {
588 let signal = Signal::new(SignalId(1), String::from("x"));
589 assert_eq!(signal.get_untracked(), "x");
590 assert_eq!(signal.read_untracked(), "x");
591 }
592}