pocopine_core/
model_runtime.rs1use std::cell::{Cell, RefCell};
2use std::collections::HashMap;
3
4use js_sys::JSON;
5use wasm_bindgen::JsValue;
6use web_sys::{CustomEvent, CustomEventInit, Element};
7
8use crate::reactive::ScopeId;
9use crate::scope::Scope;
10use crate::tick;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum WriteOrigin {
14 ParentModelIn,
15 LocalHandler,
16 SetupSeed,
17 ObserveMirror,
18}
19
20#[derive(Clone)]
21struct PendingModel {
22 wire_name: String,
23 detail: JsValue,
24 origin: WriteOrigin,
25}
26
27#[derive(Default)]
28struct ScopeModelRuntime {
29 emit_el: Option<Element>,
30 pending: HashMap<String, PendingModel>,
31}
32
33thread_local! {
34 static WRITE_ORIGIN: Cell<WriteOrigin> = const { Cell::new(WriteOrigin::LocalHandler) };
35 static MODEL_RUNTIME: RefCell<HashMap<ScopeId, ScopeModelRuntime>> =
36 RefCell::new(HashMap::new());
37 static FLUSH_PENDING: RefCell<Vec<ScopeId>> = const { RefCell::new(Vec::new()) };
38 static FLUSH_SCHEDULED: Cell<bool> = const { Cell::new(false) };
39}
40
41pub fn with_write_origin<R>(origin: WriteOrigin, f: impl FnOnce() -> R) -> R {
42 let prev = WRITE_ORIGIN.with(|c| c.replace(origin));
43 let out = f();
44 WRITE_ORIGIN.with(|c| c.set(prev));
45 out
46}
47
48pub fn current_write_origin() -> WriteOrigin {
49 WRITE_ORIGIN.with(|c| c.get())
50}
51
52pub fn capture_emit_el(scope_id: ScopeId, el: &Element) {
53 MODEL_RUNTIME.with(|m| {
54 m.borrow_mut().entry(scope_id).or_default().emit_el = Some(el.clone());
55 });
56}
57
58pub fn clear_scope(scope_id: ScopeId) {
59 MODEL_RUNTIME.with(|m| {
60 m.borrow_mut().remove(&scope_id);
61 });
62 FLUSH_PENDING.with(|q| {
63 q.borrow_mut().retain(|id| *id != scope_id);
64 });
65}
66
67pub fn emit_target(scope_id: ScopeId) -> Option<Element> {
68 MODEL_RUNTIME.with(|m| m.borrow().get(&scope_id).and_then(|rt| rt.emit_el.clone()))
69}
70
71pub fn resolve_model_key(scope_id: ScopeId, wire_name: &str) -> Option<String> {
72 let scope = Scope::find(scope_id)?;
73 let state = scope.state.borrow();
74 for key in state.keys() {
75 if state.is_model(key) && state.model_name(key) == Some(wire_name) {
76 return Some((*key).to_string());
77 }
78 }
79 if state.keys().contains(&wire_name) {
80 return Some(wire_name.to_string());
81 }
82 None
83}
84
85pub fn with_scope_write<R>(scope_id: ScopeId, origin: WriteOrigin, f: impl FnOnce() -> R) -> R {
86 let before = snapshot_models(scope_id);
87 let out = with_write_origin(origin, f);
88 let after = snapshot_models(scope_id);
89 queue_changed_models(scope_id, origin, before, after);
90 out
91}
92
93fn snapshot_models(scope_id: ScopeId) -> HashMap<String, (String, JsValue, String)> {
94 let Some(scope) = Scope::find(scope_id) else {
95 return HashMap::new();
96 };
97 let Ok(state) = scope.state.try_borrow() else {
106 return HashMap::new();
107 };
108 let mut out = HashMap::new();
109 for key in state.keys() {
110 if !state.is_model(key) {
111 continue;
112 }
113 let value = state.get_model_value(key);
114 let Some(fingerprint) = fingerprint(&value) else {
115 continue;
116 };
117 let wire_name = state.model_name(key).unwrap_or(key).to_string();
118 out.insert((*key).to_string(), (wire_name, value, fingerprint));
119 }
120 out
121}
122
123fn fingerprint(value: &JsValue) -> Option<String> {
124 if value.is_undefined() {
125 return Some("undefined".into());
126 }
127 JSON::stringify(value).ok()?.as_string()
128}
129
130fn queue_changed_models(
131 scope_id: ScopeId,
132 origin: WriteOrigin,
133 before: HashMap<String, (String, JsValue, String)>,
134 after: HashMap<String, (String, JsValue, String)>,
135) {
136 MODEL_RUNTIME.with(|m| {
137 let mut map = m.borrow_mut();
138 let runtime = map.entry(scope_id).or_default();
139 for (key, (wire_name, detail, after_fp)) in after {
140 let changed = before
141 .get(&key)
142 .map(|(_, _, before_fp)| before_fp != &after_fp)
143 .unwrap_or(true);
144 if !changed {
145 continue;
146 }
147 runtime.pending.insert(
148 key,
149 PendingModel {
150 wire_name,
151 detail,
152 origin,
153 },
154 );
155 }
156 });
157 schedule_flush(scope_id);
158}
159
160fn schedule_flush(scope_id: ScopeId) {
161 FLUSH_PENDING.with(|q| {
162 let mut q = q.borrow_mut();
163 if !q.contains(&scope_id) {
164 q.push(scope_id);
165 }
166 });
167 if FLUSH_SCHEDULED.with(|f| f.replace(true)) {
168 return;
169 }
170 tick::next(|| {
171 FLUSH_SCHEDULED.with(|f| f.set(false));
172 flush_pending();
173 });
174}
175
176fn flush_pending() {
177 let scope_ids = FLUSH_PENDING.with(|q| q.borrow_mut().drain(..).collect::<Vec<_>>());
178 for scope_id in scope_ids {
179 let (emit_el, pending) = MODEL_RUNTIME.with(|m| {
180 let mut map = m.borrow_mut();
181 let Some(runtime) = map.get_mut(&scope_id) else {
182 return (None, Vec::new());
183 };
184 let emit_el = runtime.emit_el.clone();
185 let pending = runtime
186 .pending
187 .drain()
188 .map(|(_, item)| item)
189 .collect::<Vec<_>>();
190 (emit_el, pending)
191 });
192 let Some(el) = emit_el else { continue };
193 for item in pending {
194 if matches!(
211 item.origin,
212 WriteOrigin::ParentModelIn | WriteOrigin::SetupSeed | WriteOrigin::ObserveMirror
213 ) {
214 #[cfg(any(debug_assertions, feature = "devtools"))]
215 testing::record_suppressed(scope_id, &item);
216 continue;
217 }
218 #[cfg(any(debug_assertions, feature = "devtools"))]
219 testing::record_emitted(scope_id, &item);
220 let init = CustomEventInit::new();
221 init.set_bubbles(true);
222 init.set_detail(&item.detail);
223 let name = format!("pp:update:{}", item.wire_name);
224 if let Ok(ev) = CustomEvent::new_with_event_init_dict(&name, &init) {
225 let _ = el.dispatch_event(&ev);
226 }
227 }
228 }
229}
230
231#[cfg(any(debug_assertions, feature = "devtools"))]
265pub mod testing {
266 use super::{PendingModel, WriteOrigin};
267 use crate::reactive::ScopeId;
268 use std::cell::RefCell;
269 use wasm_bindgen::JsValue;
270
271 #[derive(Clone, Debug)]
274 pub struct ModelEvent {
275 pub scope_id: ScopeId,
276 pub wire_name: String,
277 pub detail: JsValue,
278 pub origin: WriteOrigin,
279 }
280
281 thread_local! {
282 static EMITTED_LOG: RefCell<Vec<ModelEvent>> = const { RefCell::new(Vec::new()) };
283 static SUPPRESSED_LOG: RefCell<Vec<ModelEvent>> = const { RefCell::new(Vec::new()) };
284 }
285
286 pub(super) fn record_emitted(scope_id: ScopeId, item: &PendingModel) {
287 EMITTED_LOG.with(|l| {
288 l.borrow_mut().push(ModelEvent {
289 scope_id,
290 wire_name: item.wire_name.clone(),
291 detail: item.detail.clone(),
292 origin: item.origin,
293 });
294 });
295 }
296
297 pub(super) fn record_suppressed(scope_id: ScopeId, item: &PendingModel) {
298 SUPPRESSED_LOG.with(|l| {
299 l.borrow_mut().push(ModelEvent {
300 scope_id,
301 wire_name: item.wire_name.clone(),
302 detail: item.detail.clone(),
303 origin: item.origin,
304 });
305 });
306 }
307
308 pub fn drain_emitted() -> Vec<ModelEvent> {
311 EMITTED_LOG.with(|l| std::mem::take(&mut *l.borrow_mut()))
312 }
313
314 pub fn drain_suppressed() -> Vec<ModelEvent> {
318 SUPPRESSED_LOG.with(|l| std::mem::take(&mut *l.borrow_mut()))
319 }
320
321 pub fn reset() {
324 EMITTED_LOG.with(|l| l.borrow_mut().clear());
325 SUPPRESSED_LOG.with(|l| l.borrow_mut().clear());
326 super::MODEL_RUNTIME.with(|m| m.borrow_mut().clear());
327 super::FLUSH_PENDING.with(|q| q.borrow_mut().clear());
328 super::FLUSH_SCHEDULED.with(|f| f.set(false));
329 }
330}