1use parking_lot::Mutex;
8use serde::Serialize;
9use uuid::Uuid;
10
11use crate::component::MountProps;
12use crate::error::Result;
13use crate::registry::{self, BoxedComponent};
14use crate::snapshot::{self, Envelope, Memo};
15
16tokio::task_local! {
17 static REQUEST_MOUNTS: Mutex<Vec<MountInfo>>;
18 pub(crate) static CURRENT_CSRF: String;
19}
20
21static GLOBAL_MOUNTS: once_cell::sync::Lazy<Mutex<Vec<MountInfo>>> =
22 once_cell::sync::Lazy::new(|| Mutex::new(Vec::new()));
23
24#[derive(Debug, Clone, Serialize)]
25pub struct MountInfo {
26 pub id: String,
27 pub class: String,
28 pub listeners: Vec<String>,
29}
30
31fn push_mount(info: MountInfo) {
32 let pushed = REQUEST_MOUNTS.try_with(|m| {
33 m.lock().push(info.clone());
34 });
35 if pushed.is_err() {
36 GLOBAL_MOUNTS.lock().push(info);
37 }
38}
39
40pub fn drain_mounts() -> Vec<MountInfo> {
42 REQUEST_MOUNTS
43 .try_with(|m| std::mem::take(&mut *m.lock()))
44 .unwrap_or_else(|_| std::mem::take(&mut *GLOBAL_MOUNTS.lock()))
45}
46
47pub async fn with_request_scope<F, T>(fut: F) -> T
50where
51 F: std::future::Future<Output = T>,
52{
53 REQUEST_MOUNTS
54 .scope(
55 Mutex::new(Vec::new()),
56 CURRENT_CSRF.scope(String::new(), fut),
57 )
58 .await
59}
60
61pub async fn with_request_scope_csrf<F, T>(csrf: String, fut: F) -> T
63where
64 F: std::future::Future<Output = T>,
65{
66 REQUEST_MOUNTS
67 .scope(Mutex::new(Vec::new()), CURRENT_CSRF.scope(csrf, fut))
68 .await
69}
70
71pub fn signing() -> (String, bool) {
73 let container = anvil_core::container::try_current();
74 let key = container
75 .as_ref()
76 .map(|c| c.app().key.clone())
77 .filter(|k| !k.is_empty())
78 .unwrap_or_else(|| {
79 std::env::var("APP_KEY").unwrap_or_else(|_| "spark-dev-key-please-rotate".into())
80 });
81 let encrypt = std::env::var("SPARK_ENCRYPT")
82 .ok()
83 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
84 .unwrap_or(false);
85 (key, encrypt)
86}
87
88pub fn wrap(html: &str, memo: &Memo, snapshot_wire: &str) -> String {
90 let listeners_attr = if memo.listeners.is_empty() {
91 String::new()
92 } else {
93 format!(
94 r#" spark:listen="{}""#,
95 escape_attr(&memo.listeners.join(","))
96 )
97 };
98 format!(
99 r#"<div spark:id="{id}" spark:name="{view}" spark:class="{class}" spark:snapshot="{snapshot}"{listeners}>{html}</div>"#,
100 id = escape_attr(&memo.id),
101 view = escape_attr(&memo.view),
102 class = escape_attr(&memo.class),
103 snapshot = escape_attr(snapshot_wire),
104 listeners = listeners_attr,
105 html = html
106 )
107}
108
109fn escape_attr(s: &str) -> String {
110 s.replace('&', "&")
111 .replace('"', """)
112 .replace('<', "<")
113 .replace('>', ">")
114}
115
116pub fn render_mount(name: &str, props: &serde_json::Value) -> Result<String> {
119 let entry = registry::resolve(name)?;
120 let mount_props = MountProps::new(props.clone());
121 let component: BoxedComponent = (entry.mount)(mount_props);
122 let id = Uuid::now_v7().to_string();
123 let memo = Memo {
124 id: id.clone(),
125 class: component.class.to_string(),
126 view: component.view.to_string(),
127 listeners: (entry.listeners)(),
128 errors: None,
129 };
130
131 let html = component.state.render()?;
132 let data = component.state.snapshot_data();
133
134 let (app_key, encrypt) = signing();
135 let envelope = Envelope::build(&app_key, data, memo.clone());
136 let wire = snapshot::encode(&envelope, &app_key, encrypt)?;
137
138 push_mount(MountInfo {
139 id: memo.id.clone(),
140 class: memo.class.clone(),
141 listeners: memo.listeners.clone(),
142 });
143
144 Ok(wrap(&html, &memo, &wire))
145}
146
147pub fn boot_script() -> String {
150 let mounts = drain_mounts();
151 let csrf = current_csrf();
152 let endpoint = std::env::var("SPARK_UPDATE_PATH").unwrap_or_else(|_| "/_spark/update".into());
153 let runtime = std::env::var("SPARK_RUNTIME_PATH").unwrap_or_else(|_| "/_spark/spark.js".into());
154
155 #[derive(Serialize)]
156 struct Boot<'a> {
157 csrf: &'a str,
158 endpoint: &'a str,
159 mounts: &'a [MountInfo],
160 }
161 let boot = Boot {
162 csrf: &csrf,
163 endpoint: &endpoint,
164 mounts: &mounts,
165 };
166 let json = serde_json::to_string(&boot).unwrap_or_else(|_| "{}".into());
167
168 format!(
169 r#"<script src="{runtime}" defer></script>
170<script>window.__spark_boot={json};</script>"#
171 )
172}
173
174pub(crate) fn current_csrf() -> String {
175 CURRENT_CSRF
176 .try_with(|t| t.clone())
177 .unwrap_or_else(|_| String::new())
178}
179
180pub fn rerender(component: &BoxedComponent, memo: &Memo) -> Result<(String, String)> {
183 let html = component.state.render()?;
184 let data = component.state.snapshot_data();
185 let (app_key, encrypt) = signing();
186 let envelope = Envelope::build(&app_key, data, memo.clone());
187 let wire = snapshot::encode(&envelope, &app_key, encrypt)?;
188 Ok((html, wire))
189}
190
191pub fn wrap_rerender(html: &str, memo: &Memo, snapshot_wire: &str) -> String {
194 wrap(html, memo, snapshot_wire)
195}