1use std::{any::TypeId, collections::HashMap, panic::Location, sync::Arc};
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer, de::DeserializeOwned};
4use topcoat_core::context::{Cx, try_request_context};
5use topcoat_view::{
6 HoistKey, hoist, hoist_once,
7 identity::{Identity, SiteKey},
8};
9
10use crate::{Surrogate, Surrogated};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct SignalId(u128);
21
22impl SignalId {
23 #[track_caller]
31 pub(crate) fn derive(location: &Location<'_>) -> Self {
32 Self(
33 Identity::current()
34 .child(SiteKey::from_location(location))
35 .hash(),
36 )
37 }
38}
39
40impl std::fmt::Display for SignalId {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 write!(f, "{:032x}", self.0)
43 }
44}
45
46impl Serialize for SignalId {
47 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
48 serializer.collect_str(self)
49 }
50}
51
52impl<'de> Deserialize<'de> for SignalId {
53 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
54 struct Visitor;
55
56 impl serde::de::Visitor<'_> for Visitor {
57 type Value = SignalId;
58
59 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str("a hex signal id")
61 }
62
63 fn visit_str<E: serde::de::Error>(self, hex: &str) -> Result<Self::Value, E> {
64 u128::from_str_radix(hex, 16)
65 .map(SignalId)
66 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(hex), &self))
67 }
68 }
69
70 deserializer.deserialize_str(Visitor)
71 }
72}
73
74#[derive(Debug, Default, Deserialize)]
82#[serde(transparent)]
83pub struct SignalValues(HashMap<SignalId, serde_json::Value>);
84
85impl SignalValues {
86 fn get(&self, id: SignalId) -> Option<&serde_json::Value> {
88 self.0.get(&id)
89 }
90}
91
92#[derive(Debug)]
111pub struct Signal<T> {
112 id: SignalId,
113 value: Arc<T>,
115}
116
117impl<T> Signal<T> {
118 #[inline]
119 pub(crate) fn new(id: SignalId, value: T) -> Self {
120 Self {
121 id,
122 value: Arc::new(value),
123 }
124 }
125
126 pub(crate) fn id(&self) -> SignalId {
127 self.id
128 }
129
130 #[must_use]
143 #[track_caller]
144 pub fn read(&self) -> &T {
145 self.track();
146 &self.value
147 }
148
149 #[must_use]
154 pub fn read_untracked(&self) -> &T {
155 &self.value
156 }
157
158 #[track_caller]
161 fn track(&self) {
162 let id = self.id;
163 hoist_once(
164 HoistKey::new((TypeId::of::<SignalId>(), id)),
165 move |parts| {
166 parts.push_comment(|comment| {
167 comment
168 .push_promoted_str_unescaped(&"::topcoat::dep(\"")
169 .push_string_unescaped(id.to_string())
170 .push_promoted_str_unescaped(&"\")");
171 });
172 },
173 );
174 }
175
176 fn declaration(&self) -> String
179 where
180 T: SignalValue,
181 {
182 #[derive(Serialize)]
183 struct Declaration<'a, V>
184 where
185 V: ?Sized,
186 {
187 t: &'static str,
188 id: SignalId,
189 v: &'a V,
190 }
191
192 let value = self.value.surrogate();
193 let declaration = Declaration {
194 t: "signal",
195 id: self.id,
196 v: &value,
197 };
198 serde_json::to_string(&declaration).expect("failed to serialize signal declaration")
199 }
200}
201
202impl<T> Signal<T>
203where
204 T: Clone,
205{
206 #[must_use]
217 #[track_caller]
218 pub fn get(&self) -> T {
219 self.track();
220 T::clone(&self.value)
221 }
222
223 #[must_use]
228 pub fn get_untracked(&self) -> T {
229 T::clone(&self.value)
230 }
231}
232
233impl<T> Clone for Signal<T> {
234 fn clone(&self) -> Self {
235 Self {
236 id: self.id,
237 value: Arc::clone(&self.value),
238 }
239 }
240}
241
242pub trait SignalValue: Sized {
249 type Surrogate<'a>: Serialize
251 where
252 Self: 'a;
253
254 fn surrogate(&self) -> Self::Surrogate<'_>;
256
257 fn from_value(value: &serde_json::Value) -> Option<Self>;
260}
261
262impl<T> SignalValue for T
263where
264 T: Surrogated,
265 T::Surrogate: DeserializeOwned,
266 for<'a> &'a T: Surrogated,
267 for<'a> <&'a T as Surrogated>::Surrogate: Serialize,
268{
269 type Surrogate<'a>
270 = <&'a T as Surrogated>::Surrogate
271 where
272 Self: 'a;
273
274 fn surrogate(&self) -> Self::Surrogate<'_> {
275 self.into_surrogate()
276 }
277
278 fn from_value(value: &serde_json::Value) -> Option<Self> {
279 T::Surrogate::deserialize(value)
280 .ok()
281 .map(Surrogate::into_real)
282 }
283}
284
285#[track_caller]
333pub fn signal<T>(cx: &Cx, init: impl FnOnce() -> T) -> Signal<T>
334where
335 T: SignalValue,
336{
337 let id = SignalId::derive(Location::caller());
338 let value = try_request_context::<SignalValues>(cx)
339 .and_then(|values| values.get(id))
340 .and_then(T::from_value)
341 .unwrap_or_else(init);
342 let signal = Signal::new(id, value);
343 let declaration = signal.declaration();
344 hoist(move |parts| {
345 parts.push_comment(|comment| {
346 comment
349 .push_promoted_str_unescaped(&"::topcoat::signal(")
350 .push_string(declaration)
351 .push_promoted_str_unescaped(&")");
352 });
353 });
354 signal
355}
356
357#[cfg(test)]
358mod tests {
359 use std::{
360 panic::{AssertUnwindSafe, catch_unwind},
361 pin::pin,
362 sync::{
363 Arc, OnceLock,
364 atomic::{AtomicBool, Ordering},
365 },
366 task::{Context, Poll, Waker},
367 };
368
369 use topcoat::view::{
370 HoistView, ViewExt,
371 identity::{IdentityGuard, IdentityView},
372 internal::ThenView,
373 view,
374 };
375
376 use super::*;
377
378 const SITE_A: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
379 const SITE_B: SiteKey = SiteKey::new(file!(), line!(), column!(), 0);
380
381 fn block_on<F: Future>(future: F) -> F::Output {
383 let mut future = pin!(future);
384 let mut cx = Context::from_waker(Waker::noop());
385 loop {
386 if let Poll::Ready(output) = future.as_mut().poll(&mut cx) {
387 return output;
388 }
389 }
390 }
391
392 fn render_with_signal(value: &'static str) -> String {
395 let cx = &Cx::default();
396 let view = HoistView::new(ThenView::new(async move {
397 let signal = signal(cx, || String::from(value));
398 Ok(view! { cx => <p>(signal.read_untracked())</p> })
399 }));
400 block_on(view.single()).unwrap().render(cx)
401 }
402
403 fn render_reading(read: impl Fn(&Signal<String>) + Send + 'static) -> (String, String) {
406 let cx = &Cx::default();
407 let id = Arc::new(OnceLock::new());
408 let out = Arc::clone(&id);
409 let view = HoistView::new(ThenView::new(async move {
410 let signal = signal(cx, || String::from("x"));
411 read(&signal);
412 out.set(signal.id()).unwrap();
413 Ok(view! { cx => <p></p> })
414 }));
415 let html = block_on(view.single()).unwrap().render(cx);
416 let marker = format!("<!--::topcoat::dep(\"{}\")-->", id.get().unwrap());
417 (html, marker)
418 }
419
420 fn signal_id_at(site: SiteKey) -> SignalId {
423 let cx = &Cx::default();
424 let identity = IdentityGuard::enter(site).identity();
425 let view = IdentityView::new(
426 identity,
427 HoistView::new(ThenView::new(async move {
428 let signal = signal(cx, || 0.0_f64);
429 Ok(view! { cx => <p>(signal.id().to_string())</p> })
430 })),
431 );
432 let html = block_on(view.single()).unwrap().render(cx);
433 let start = html.find("<p>").unwrap() + 3;
434 let end = html.rfind("</p>").unwrap();
435 SignalId(u128::from_str_radix(&html[start..end], 16).unwrap())
436 }
437
438 #[test]
439 fn creating_a_signal_outside_a_body_panics() {
440 let cx = Cx::default();
441 let panic = catch_unwind(AssertUnwindSafe(|| signal(&cx, || 0.0_f64))).unwrap_err();
442 let message = panic.downcast::<&str>().expect("panics with a message");
443 assert!(message.contains("no view is collecting hoisted parts"));
444 }
445
446 #[test]
447 fn the_same_call_site_renders_the_same_id_every_time() {
448 assert_eq!(render_with_signal("x"), render_with_signal("x"));
449 assert_eq!(signal_id_at(SITE_A), signal_id_at(SITE_A));
450 }
451
452 #[test]
453 fn distinct_call_sites_render_distinct_ids() {
454 let cx = &Cx::default();
455 let view = HoistView::new(ThenView::new(async move {
456 let first = signal(cx, || 0.0_f64);
457 let second = signal(cx, || 0.0_f64);
458 assert_ne!(first.id(), second.id());
459 Ok(view! { cx => <p></p> })
460 }));
461 block_on(view.single()).unwrap();
462 }
463
464 #[test]
465 fn distinct_identities_render_distinct_ids() {
466 assert_ne!(signal_id_at(SITE_A), signal_id_at(SITE_B));
467 }
468
469 #[test]
470 fn an_ambiguous_identity_panics() {
471 let cx = Cx::default();
472 let _guard = IdentityGuard::enter_ambiguous(SITE_A, "`card` at src/a.rs:1");
473 let panic = catch_unwind(AssertUnwindSafe(|| signal(&cx, || 0.0_f64))).unwrap_err();
474 let message = panic.downcast::<String>().expect("panics with a message");
475 assert!(message.contains("`card` at src/a.rs:1"), "{message}");
476 }
477
478 fn number_signal(cx: &Cx) -> (SignalId, f64, bool) {
481 let seen = Arc::new(OnceLock::new());
482 let out = Arc::clone(&seen);
483 let view = HoistView::new(ThenView::new(async move {
484 let init_ran = AtomicBool::new(false);
485 let signal = signal(cx, || {
486 init_ran.store(true, Ordering::Relaxed);
487 1.0_f64
488 });
489 out.set((
490 signal.id(),
491 *signal.read_untracked(),
492 init_ran.load(Ordering::Relaxed),
493 ))
494 .unwrap();
495 Ok(view! { cx => <p></p> })
496 }));
497 block_on(view.single()).unwrap();
498 *seen.get().unwrap()
499 }
500
501 fn cx_carrying(id: SignalId, value: serde_json::Value) -> Cx {
503 Cx::default().with(SignalValues(HashMap::from([(id, value)])))
504 }
505
506 #[test]
507 fn a_signal_resumes_from_the_value_the_request_carries() {
508 let (id, value, init_ran) = number_signal(&Cx::default());
509 assert_eq!((value, init_ran), (1.0, true));
510
511 let cx = cx_carrying(id, serde_json::json!(5.0));
512 assert_eq!(number_signal(&cx), (id, 5.0, false));
513 }
514
515 #[test]
516 fn a_value_that_does_not_fit_the_signal_is_ignored() {
517 let (id, ..) = number_signal(&Cx::default());
518
519 let cx = cx_carrying(id, serde_json::json!("five"));
520 assert_eq!(number_signal(&cx), (id, 1.0, true));
521 }
522
523 #[test]
524 fn an_id_round_trips_through_json_as_hex() {
525 let id = SignalId(0x1234_abcd);
526 let json = serde_json::to_string(&id).unwrap();
527 assert_eq!(json, format!("\"{:032x}\"", 0x1234_abcd_u128));
528 assert_eq!(serde_json::from_str::<SignalId>(&json).unwrap(), id);
529 assert!(serde_json::from_str::<SignalId>("\"zz\"").is_err());
530 }
531
532 #[test]
533 fn the_declaration_renders_ahead_of_the_content() {
534 let html = render_with_signal("x");
535 assert!(html.starts_with("<!--::topcoat::signal("), "{html}");
536 assert!(html.ends_with("--><p>x</p>"), "{html}");
537 }
538
539 #[test]
540 fn payload_cannot_terminate_the_comment() {
541 let html = render_with_signal("a-->b\"c&d");
544
545 assert_eq!(html.matches("-->").count(), 1, "{html}");
548 assert!(html.contains("-->"), "{html}");
549 assert!(html.contains("""), "{html}");
551 }
552
553 #[test]
554 fn a_tracked_read_renders_a_dependency_marker_after_the_declaration() {
555 let (html, marker) = render_reading(|signal| {
556 assert_eq!(signal.get(), "x");
557 });
558 assert!(html.starts_with("<!--::topcoat::signal("), "{html}");
559 assert!(html.ends_with(&format!("{marker}<p></p>")), "{html}");
560 }
561
562 #[test]
563 fn repeated_reads_render_one_dependency_marker() {
564 let (html, marker) = render_reading(|signal| {
565 let _ = signal.get();
566 let _ = signal.read();
567 let _ = signal.get();
568 });
569 assert_eq!(html.matches(&marker).count(), 1, "{html}");
570 }
571
572 #[test]
573 fn untracked_reads_render_no_dependency_marker() {
574 let (html, _) = render_reading(|signal| {
575 assert_eq!(signal.get_untracked(), "x");
576 assert_eq!(signal.read_untracked(), "x");
577 });
578 assert!(!html.contains("::topcoat::dep("), "{html}");
579 }
580
581 #[test]
582 fn a_read_in_a_runtime_expression_renders_no_dependency_marker() {
583 let (html, _) = render_reading(|signal| {
584 let surrogate = crate::SignalSurrogate::new(signal.clone());
585 let _ = surrogate.get();
586 let _ = surrogate.read();
587 });
588 assert!(!html.contains("::topcoat::dep("), "{html}");
589 }
590
591 #[test]
592 fn a_tracked_read_outside_a_body_panics() {
593 let signal = Signal::new(SignalId(1), String::from("x"));
594 let panic = catch_unwind(AssertUnwindSafe(|| signal.get())).unwrap_err();
595 let message = panic.downcast::<&str>().expect("panics with a message");
596 assert!(message.contains("no view is collecting hoisted parts"));
597 }
598
599 #[test]
600 fn untracked_reads_work_outside_a_body() {
601 let signal = Signal::new(SignalId(1), String::from("x"));
602 assert_eq!(signal.get_untracked(), "x");
603 assert_eq!(signal.read_untracked(), "x");
604 }
605}