1use std::collections::{BTreeMap, BTreeSet};
2use std::rc::Rc;
3
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{
8 ComponentInstancePath, ComponentStateSchema, SchemaValidationError, UiValue, UiValuePath,
9 UiValuePathError,
10};
11
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
13#[serde(tag = "scope", content = "id", rename_all = "snake_case")]
14pub enum StoreScope {
15 App,
16 Window(String),
17}
18
19#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
20pub struct StoreId {
21 pub scope: StoreScope,
22 pub name: String,
23}
24
25impl StoreId {
26 #[must_use]
27 pub fn app(name: impl Into<String>) -> Self {
28 Self {
29 scope: StoreScope::App,
30 name: name.into(),
31 }
32 }
33
34 #[must_use]
35 pub fn window(window: impl Into<String>, name: impl Into<String>) -> Self {
36 Self {
37 scope: StoreScope::Window(window.into()),
38 name: name.into(),
39 }
40 }
41}
42
43#[derive(Clone, Debug, Default)]
44pub struct StoreRegistry {
45 stores: Rc<BTreeMap<StoreId, StoreState>>,
46 readers: Rc<BTreeMap<StoreField, BTreeSet<crate::read_dependency::ReadDependency>>>,
47}
48
49#[derive(Clone, Debug)]
50struct StoreState {
51 schema: Rc<ComponentStateSchema>,
52 values: Rc<BTreeMap<String, UiValue>>,
53}
54
55#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
56struct StoreField {
57 store: StoreId,
58 field: String,
59 path: Option<UiValuePath>,
60}
61
62impl StoreRegistry {
63 #[must_use]
64 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn declare(&mut self, id: StoreId, schema: ComponentStateSchema) -> Result<(), StoreError> {
74 if self.stores.contains_key(&id) {
75 return Err(StoreError::DuplicateStore(id));
76 }
77 ComponentStateSchema::new(schema.fields().clone())?;
78 let values = schema
79 .fields()
80 .iter()
81 .map(|(name, field)| (name.clone(), field.default.clone()))
82 .collect();
83 Rc::make_mut(&mut self.stores).insert(
84 id,
85 StoreState {
86 schema: Rc::new(schema),
87 values: Rc::new(values),
88 },
89 );
90 Ok(())
91 }
92
93 pub fn begin_read<'a>(&'a mut self, reader: &'a ComponentInstancePath) -> StoreReadSession<'a> {
98 self.reset_reader(reader);
99 StoreReadSession {
100 registry: self,
101 reader,
102 }
103 }
104
105 pub fn reset_reader(&mut self, reader: &ComponentInstancePath) {
106 self.reset_contribution(&crate::read_dependency::ReadDependency::component(reader));
107 }
108 pub(crate) fn reset_contribution(&mut self, reader: &crate::read_dependency::ReadDependency) {
109 let tracked = Rc::make_mut(&mut self.readers);
110 for readers in tracked.values_mut() {
111 readers.remove(reader);
112 }
113 tracked.retain(|_, readers| !readers.is_empty());
114 }
115
116 pub(crate) fn retain_contributions(
118 &mut self,
119 scope: &ComponentInstancePath,
120 active: &BTreeSet<crate::read_dependency::ReadContribution>,
121 ) {
122 crate::read_dependency::retain_readers(Rc::make_mut(&mut self.readers), |reader| {
123 reader.retained_in_contribution_scope(scope, active)
124 });
125 }
126
127 pub fn retain_reader_scope(
129 &mut self,
130 root: &ComponentInstancePath,
131 active: &BTreeSet<ComponentInstancePath>,
132 ) {
133 let tracked = Rc::make_mut(&mut self.readers);
134 for readers in tracked.values_mut() {
135 readers.retain(|reader| reader.retained_in_owner_scope(root, active));
136 }
137 tracked.retain(|_, readers| !readers.is_empty());
138 }
139
140 pub fn read_tracked(
146 &mut self,
147 reader: &ComponentInstancePath,
148 store: &StoreId,
149 field: &str,
150 ) -> Result<UiValue, StoreError> {
151 self.read_dependency(
152 &crate::read_dependency::ReadDependency::component(reader),
153 store,
154 field,
155 )
156 }
157 pub(crate) fn read_dependency(
158 &mut self,
159 reader: &crate::read_dependency::ReadDependency,
160 store: &StoreId,
161 field: &str,
162 ) -> Result<UiValue, StoreError> {
163 let value = self
164 .stores
165 .get(store)
166 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
167 .values
168 .get(field)
169 .cloned()
170 .ok_or_else(|| StoreError::UnknownField {
171 store: store.clone(),
172 field: field.to_owned(),
173 })?;
174 Rc::make_mut(&mut self.readers)
175 .entry(StoreField {
176 store: store.clone(),
177 field: field.to_owned(),
178 path: None,
179 })
180 .or_default()
181 .insert(reader.clone());
182 Ok(value)
183 }
184
185 pub fn read_path_tracked(
191 &mut self,
192 reader: &ComponentInstancePath,
193 store: &StoreId,
194 field: &str,
195 path: &UiValuePath,
196 ) -> Result<UiValue, StoreError> {
197 self.read_path_dependency(
198 &crate::read_dependency::ReadDependency::component(reader),
199 store,
200 field,
201 path,
202 )
203 }
204 pub(crate) fn read_path_dependency(
205 &mut self,
206 reader: &crate::read_dependency::ReadDependency,
207 store: &StoreId,
208 field: &str,
209 path: &UiValuePath,
210 ) -> Result<UiValue, StoreError> {
211 let value = self
212 .stores
213 .get(store)
214 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
215 .values
216 .get(field)
217 .ok_or_else(|| StoreError::UnknownField {
218 store: store.clone(),
219 field: field.to_owned(),
220 })?
221 .get_path(path)?
222 .clone();
223 Rc::make_mut(&mut self.readers)
224 .entry(StoreField {
225 store: store.clone(),
226 field: field.to_owned(),
227 path: Some(path.clone()),
228 })
229 .or_default()
230 .insert(reader.clone());
231 Ok(value)
232 }
233
234 pub fn write(
241 &mut self,
242 store: &StoreId,
243 field: &str,
244 value: UiValue,
245 ) -> Result<BTreeSet<ComponentInstancePath>, StoreError> {
246 let state = self
247 .stores
248 .get(store)
249 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?;
250 let declared = state
251 .schema
252 .field(field)
253 .ok_or_else(|| StoreError::UnknownField {
254 store: store.clone(),
255 field: field.to_owned(),
256 })?;
257 declared
258 .schema
259 .validate_ui_value(&value)
260 .map_err(|source| StoreError::InvalidValue {
261 store: store.clone(),
262 field: field.to_owned(),
263 source,
264 })?;
265
266 let previous =
267 state
268 .values
269 .get(field)
270 .cloned()
271 .ok_or_else(|| StoreError::UnknownField {
272 store: store.clone(),
273 field: field.to_owned(),
274 })?;
275 if previous == value {
276 return Ok(BTreeSet::new());
277 }
278 let invalidated = self
279 .readers
280 .iter()
281 .filter(|(read, _)| read.store == *store && read.field == field)
282 .filter(|(read, _)| {
283 read.path
284 .as_ref()
285 .is_none_or(|path| previous.get_path(path).ok() != value.get_path(path).ok())
286 })
287 .flat_map(|(_, readers)| readers.iter().map(|reader| reader.owner.clone()))
288 .collect();
289 let state = Rc::make_mut(&mut self.stores)
290 .get_mut(store)
291 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?;
292 Rc::make_mut(&mut state.values).insert(field.to_owned(), value);
293 Ok(invalidated)
294 }
295
296 pub fn write_path(
302 &mut self,
303 store: &StoreId,
304 field: &str,
305 path: &UiValuePath,
306 value: UiValue,
307 ) -> Result<BTreeSet<ComponentInstancePath>, StoreError> {
308 let mut root = self
309 .stores
310 .get(store)
311 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
312 .values
313 .get(field)
314 .cloned()
315 .ok_or_else(|| StoreError::UnknownField {
316 store: store.clone(),
317 field: field.to_owned(),
318 })?;
319 root.set_path(path, value)?;
320 self.write(store, field, root)
321 }
322
323 pub fn remove_window(&mut self, window: &str) {
325 Rc::make_mut(&mut self.stores)
326 .retain(|id, _| !matches!(&id.scope, StoreScope::Window(id) if id == window));
327 Rc::make_mut(&mut self.readers).retain(
328 |field, _| !matches!(&field.store.scope, StoreScope::Window(id) if id == window),
329 );
330 }
331
332 #[must_use]
333 pub fn is_sensitive(&self, store: &StoreId, field: &str) -> bool {
334 self.stores
335 .get(store)
336 .and_then(|state| state.schema.field(field))
337 .is_some_and(|field| field.sensitive)
338 }
339
340 #[must_use]
341 pub fn inspect(&self) -> Vec<StoreSnapshot> {
342 self.stores
343 .iter()
344 .map(|(id, state)| StoreSnapshot {
345 id: id.clone(),
346 fields: state
347 .values
348 .iter()
349 .map(|(name, value)| {
350 let sensitive = state
351 .schema
352 .field(name)
353 .is_some_and(|field| field.sensitive);
354 (
355 name.clone(),
356 crate::StateValueSnapshot {
357 value: if sensitive {
358 UiValue::String("<sensitive>".to_owned())
359 } else {
360 value.clone()
361 },
362 sensitive,
363 },
364 )
365 })
366 .collect(),
367 })
368 .collect()
369 }
370}
371
372#[derive(Clone, Debug, PartialEq)]
373pub struct StoreSnapshot {
374 pub id: StoreId,
375 pub fields: BTreeMap<String, crate::StateValueSnapshot>,
376}
377
378pub struct StoreReadSession<'a> {
379 registry: &'a mut StoreRegistry,
380 reader: &'a ComponentInstancePath,
381}
382
383impl StoreReadSession<'_> {
384 pub fn read(&mut self, store: &StoreId, field: &str) -> Result<UiValue, StoreError> {
390 self.registry.read_tracked(self.reader, store, field)
391 }
392
393 pub fn read_path(
399 &mut self,
400 store: &StoreId,
401 field: &str,
402 path: &UiValuePath,
403 ) -> Result<UiValue, StoreError> {
404 self.registry
405 .read_path_tracked(self.reader, store, field, path)
406 }
407}
408
409#[derive(Clone, Debug, Error, Eq, PartialEq)]
410pub enum StoreError {
411 #[error("store `{0:?}` is already declared")]
412 DuplicateStore(StoreId),
413 #[error("store `{0:?}` is not declared")]
414 UnknownStore(StoreId),
415 #[error("field `{field}` is not declared in store `{store:?}`")]
416 UnknownField { store: StoreId, field: String },
417 #[error("value for `{store:?}.{field}` is invalid: {source}")]
418 InvalidValue {
419 store: StoreId,
420 field: String,
421 source: SchemaValidationError,
422 },
423 #[error(transparent)]
424 State(#[from] crate::StateError),
425 #[error(transparent)]
426 Path(#[from] UiValuePathError),
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::{StateField, ValueSchema};
433
434 fn session_schema() -> ComponentStateSchema {
435 ComponentStateSchema::new(BTreeMap::from([
436 (
437 "project".to_owned(),
438 StateField::new(ValueSchema::string(), UiValue::String("alpha".to_owned())),
439 ),
440 (
441 "sidebar_open".to_owned(),
442 StateField::new(ValueSchema::Bool, UiValue::Bool(true)),
443 ),
444 ]))
445 .unwrap()
446 }
447
448 fn nested_schema() -> ComponentStateSchema {
449 let row = |id: &str, label: &str| {
450 UiValue::Map(BTreeMap::from([
451 ("id".to_owned(), UiValue::String(id.to_owned())),
452 ("label".to_owned(), UiValue::String(label.to_owned())),
453 ]))
454 };
455 ComponentStateSchema::new(BTreeMap::from([(
456 "data".to_owned(),
457 StateField::new(
458 ValueSchema::UiValue,
459 UiValue::Map(BTreeMap::from([
460 (
461 "profile".to_owned(),
462 UiValue::Map(BTreeMap::from([
463 ("name".to_owned(), UiValue::String("Ada".to_owned())),
464 ("avatar".to_owned(), UiValue::String("ada.png".to_owned())),
465 ])),
466 ),
467 (
468 "rows".to_owned(),
469 UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
470 ),
471 ])),
472 ),
473 )]))
474 .unwrap()
475 }
476
477 #[test]
478 fn writes_invalidate_only_exact_field_readers() {
479 let store_id = StoreId::app("session");
480 let project = ComponentInstancePath::root("ProjectView", "project");
481 let sidebar = ComponentInstancePath::root("Sidebar", "sidebar");
482 let mut stores = StoreRegistry::new();
483 stores.declare(store_id.clone(), session_schema()).unwrap();
484
485 stores
486 .begin_read(&project)
487 .read(&store_id, "project")
488 .unwrap();
489 stores
490 .begin_read(&sidebar)
491 .read(&store_id, "sidebar_open")
492 .unwrap();
493
494 let invalidated = stores
495 .write(&store_id, "project", UiValue::String("beta".to_owned()))
496 .unwrap();
497 assert_eq!(invalidated, BTreeSet::from([project]));
498 }
499
500 #[test]
501 fn deserialized_schema_defaults_are_validated_before_store_declaration() {
502 let schema: ComponentStateSchema = serde_json::from_str(
503 r#"{"fields":{"count":{"schema":{"type":"integer"},"default":{"type":"string","value":"wrong"}}}}"#,
504 )
505 .unwrap();
506 let mut stores = StoreRegistry::new();
507 assert!(matches!(
508 stores.declare(StoreId::app("invalid"), schema),
509 Err(StoreError::State(crate::StateError::InvalidDefault { .. }))
510 ));
511 }
512
513 #[test]
514 fn snapshots_share_large_store_values_until_a_write() {
515 let id = StoreId::app("session");
516 let mut stores = StoreRegistry::new();
517 stores.declare(id.clone(), session_schema()).unwrap();
518 let snapshot = stores.clone();
519 assert!(Rc::ptr_eq(&stores.stores, &snapshot.stores));
520 stores
521 .write(&id, "sidebar_open", UiValue::Bool(false))
522 .unwrap();
523 assert!(!Rc::ptr_eq(&stores.stores, &snapshot.stores));
524 assert_eq!(
525 snapshot.stores[&id].values["sidebar_open"],
526 UiValue::Bool(true)
527 );
528 }
529
530 #[test]
531 fn repeated_value_does_not_invalidate() {
532 let store_id = StoreId::app("session");
533 let reader = ComponentInstancePath::root("App", "root");
534 let mut stores = StoreRegistry::new();
535 stores.declare(store_id.clone(), session_schema()).unwrap();
536 stores
537 .begin_read(&reader)
538 .read(&store_id, "project")
539 .unwrap();
540
541 assert!(
542 stores
543 .write(&store_id, "project", UiValue::String("alpha".to_owned()),)
544 .unwrap()
545 .is_empty()
546 );
547 }
548
549 #[test]
550 fn removing_window_clears_window_store() {
551 let store_id = StoreId::window("settings", "draft");
552 let mut stores = StoreRegistry::new();
553 stores.declare(store_id.clone(), session_schema()).unwrap();
554 stores.remove_window("settings");
555 assert!(matches!(
556 stores.write(&store_id, "sidebar_open", UiValue::Bool(false)),
557 Err(StoreError::UnknownStore(_))
558 ));
559 }
560
561 #[test]
562 fn successful_subtree_reconcile_removes_stale_app_store_readers() {
563 let id = StoreId::app("shared");
564 let root = ComponentInstancePath::root("App", "main");
565 let removed = root.child("Panel", "removed");
566 let mut stores = StoreRegistry::new();
567 stores.declare(id.clone(), session_schema()).unwrap();
568 stores.read_tracked(&removed, &id, "sidebar_open").unwrap();
569 stores.retain_reader_scope(&root, &BTreeSet::new());
570 assert!(
571 stores
572 .write(&id, "sidebar_open", UiValue::Bool(false))
573 .unwrap()
574 .is_empty()
575 );
576 }
577
578 #[test]
579 fn nested_writes_invalidate_only_changed_path_and_whole_readers() {
580 let store_id = StoreId::app("model");
581 let whole = ComponentInstancePath::root("Whole", "whole");
582 let name = ComponentInstancePath::root("Name", "name");
583 let avatar = ComponentInstancePath::root("Avatar", "avatar");
584 let mut stores = StoreRegistry::new();
585 stores.declare(store_id.clone(), nested_schema()).unwrap();
586 let name_path = UiValuePath::new(vec![
587 crate::UiValuePathSegment::Key("profile".to_owned()),
588 crate::UiValuePathSegment::Key("name".to_owned()),
589 ])
590 .unwrap();
591 let avatar_path = UiValuePath::new(vec![
592 crate::UiValuePathSegment::Key("profile".to_owned()),
593 crate::UiValuePathSegment::Key("avatar".to_owned()),
594 ])
595 .unwrap();
596 stores.read_tracked(&whole, &store_id, "data").unwrap();
597 stores
598 .read_path_tracked(&name, &store_id, "data", &name_path)
599 .unwrap();
600 stores
601 .read_path_tracked(&avatar, &store_id, "data", &avatar_path)
602 .unwrap();
603
604 assert_eq!(
605 stores
606 .write_path(
607 &store_id,
608 "data",
609 &name_path,
610 UiValue::String("Grace".to_owned()),
611 )
612 .unwrap(),
613 BTreeSet::from([whole, name])
614 );
615 assert!(
616 stores
617 .write_path(
618 &store_id,
619 "data",
620 &name_path,
621 UiValue::String("Grace".to_owned()),
622 )
623 .unwrap()
624 .is_empty()
625 );
626 }
627
628 #[test]
629 fn keyed_item_reader_ignores_reorder_but_observes_item_change() {
630 let store_id = StoreId::app("model");
631 let reader = ComponentInstancePath::root("Row", "beta");
632 let whole = ComponentInstancePath::root("Whole", "whole");
633 let mut stores = StoreRegistry::new();
634 stores.declare(store_id.clone(), nested_schema()).unwrap();
635 let row_path = UiValuePath::new(vec![
636 crate::UiValuePathSegment::Key("rows".to_owned()),
637 crate::UiValuePathSegment::Item {
638 key_field: "id".to_owned(),
639 key: "beta".to_owned(),
640 },
641 crate::UiValuePathSegment::Key("label".to_owned()),
642 ])
643 .unwrap();
644 stores
645 .read_path_tracked(&reader, &store_id, "data", &row_path)
646 .unwrap();
647 stores.read_tracked(&whole, &store_id, "data").unwrap();
648
649 let mut reordered = stores
650 .stores
651 .get(&store_id)
652 .unwrap()
653 .values
654 .get("data")
655 .unwrap()
656 .clone();
657 if let UiValue::Map(root) = &mut reordered
658 && let Some(UiValue::Array(rows)) = root.get_mut("rows")
659 {
660 rows.reverse();
661 }
662 assert_eq!(
663 stores.write(&store_id, "data", reordered).unwrap(),
664 BTreeSet::from([whole.clone()])
665 );
666 assert_eq!(
667 stores
668 .write_path(
669 &store_id,
670 "data",
671 &row_path,
672 UiValue::String("Updated".to_owned()),
673 )
674 .unwrap(),
675 BTreeSet::from([whole, reader])
676 );
677 }
678}