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 Some(&crate::read_dependency::ReadDependency::component(reader)),
153 store,
154 field,
155 )
156 }
157 pub(crate) fn read_dependency(
159 &mut self,
160 reader: Option<&crate::read_dependency::ReadDependency>,
161 store: &StoreId,
162 field: &str,
163 ) -> Result<UiValue, StoreError> {
164 let value = self
165 .stores
166 .get(store)
167 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
168 .values
169 .get(field)
170 .cloned()
171 .ok_or_else(|| StoreError::UnknownField {
172 store: store.clone(),
173 field: field.to_owned(),
174 })?;
175 if let Some(reader) = reader {
176 Rc::make_mut(&mut self.readers)
177 .entry(StoreField {
178 store: store.clone(),
179 field: field.to_owned(),
180 path: None,
181 })
182 .or_default()
183 .insert(reader.clone());
184 }
185 Ok(value)
186 }
187
188 pub fn read_path_tracked(
194 &mut self,
195 reader: &ComponentInstancePath,
196 store: &StoreId,
197 field: &str,
198 path: &UiValuePath,
199 ) -> Result<UiValue, StoreError> {
200 self.read_path_dependency(
201 Some(&crate::read_dependency::ReadDependency::component(reader)),
202 store,
203 field,
204 path,
205 )
206 }
207 pub(crate) fn read_path_dependency(
209 &mut self,
210 reader: Option<&crate::read_dependency::ReadDependency>,
211 store: &StoreId,
212 field: &str,
213 path: &UiValuePath,
214 ) -> Result<UiValue, StoreError> {
215 let value = self
216 .stores
217 .get(store)
218 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
219 .values
220 .get(field)
221 .ok_or_else(|| StoreError::UnknownField {
222 store: store.clone(),
223 field: field.to_owned(),
224 })?
225 .get_path(path)?
226 .clone();
227 if let Some(reader) = reader {
228 Rc::make_mut(&mut self.readers)
229 .entry(StoreField {
230 store: store.clone(),
231 field: field.to_owned(),
232 path: Some(path.clone()),
233 })
234 .or_default()
235 .insert(reader.clone());
236 }
237 Ok(value)
238 }
239
240 pub fn write(
247 &mut self,
248 store: &StoreId,
249 field: &str,
250 value: UiValue,
251 ) -> Result<BTreeSet<ComponentInstancePath>, StoreError> {
252 let state = self
253 .stores
254 .get(store)
255 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?;
256 let declared = state
257 .schema
258 .field(field)
259 .ok_or_else(|| StoreError::UnknownField {
260 store: store.clone(),
261 field: field.to_owned(),
262 })?;
263 declared
264 .schema
265 .validate_ui_value(&value)
266 .map_err(|source| StoreError::InvalidValue {
267 store: store.clone(),
268 field: field.to_owned(),
269 source,
270 })?;
271
272 let previous =
273 state
274 .values
275 .get(field)
276 .cloned()
277 .ok_or_else(|| StoreError::UnknownField {
278 store: store.clone(),
279 field: field.to_owned(),
280 })?;
281 if previous == value {
282 return Ok(BTreeSet::new());
283 }
284 let invalidated = self
285 .readers
286 .iter()
287 .filter(|(read, _)| read.store == *store && read.field == field)
288 .filter(|(read, _)| {
289 read.path
290 .as_ref()
291 .is_none_or(|path| previous.get_path(path).ok() != value.get_path(path).ok())
292 })
293 .flat_map(|(_, readers)| readers.iter().map(|reader| reader.owner.clone()))
294 .collect();
295 let state = Rc::make_mut(&mut self.stores)
296 .get_mut(store)
297 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?;
298 Rc::make_mut(&mut state.values).insert(field.to_owned(), value);
299 Ok(invalidated)
300 }
301
302 pub fn write_path(
308 &mut self,
309 store: &StoreId,
310 field: &str,
311 path: &UiValuePath,
312 value: UiValue,
313 ) -> Result<BTreeSet<ComponentInstancePath>, StoreError> {
314 let mut root = self
315 .stores
316 .get(store)
317 .ok_or_else(|| StoreError::UnknownStore(store.clone()))?
318 .values
319 .get(field)
320 .cloned()
321 .ok_or_else(|| StoreError::UnknownField {
322 store: store.clone(),
323 field: field.to_owned(),
324 })?;
325 root.set_path(path, value)?;
326 self.write(store, field, root)
327 }
328
329 pub fn remove_window(&mut self, window: &str) {
331 Rc::make_mut(&mut self.stores)
332 .retain(|id, _| !matches!(&id.scope, StoreScope::Window(id) if id == window));
333 Rc::make_mut(&mut self.readers).retain(
334 |field, _| !matches!(&field.store.scope, StoreScope::Window(id) if id == window),
335 );
336 }
337
338 #[must_use]
339 pub fn is_sensitive(&self, store: &StoreId, field: &str) -> bool {
340 self.stores
341 .get(store)
342 .and_then(|state| state.schema.field(field))
343 .is_some_and(|field| field.sensitive)
344 }
345
346 #[must_use]
347 pub fn inspect(&self) -> Vec<StoreSnapshot> {
348 self.stores
349 .iter()
350 .map(|(id, state)| StoreSnapshot {
351 id: id.clone(),
352 fields: state
353 .values
354 .iter()
355 .map(|(name, value)| {
356 let sensitive = state
357 .schema
358 .field(name)
359 .is_some_and(|field| field.sensitive);
360 (
361 name.clone(),
362 crate::StateValueSnapshot {
363 value: if sensitive {
364 UiValue::String("<sensitive>".to_owned())
365 } else {
366 value.clone()
367 },
368 sensitive,
369 },
370 )
371 })
372 .collect(),
373 })
374 .collect()
375 }
376}
377
378#[derive(Clone, Debug, PartialEq)]
379pub struct StoreSnapshot {
380 pub id: StoreId,
381 pub fields: BTreeMap<String, crate::StateValueSnapshot>,
382}
383
384pub struct StoreReadSession<'a> {
385 registry: &'a mut StoreRegistry,
386 reader: &'a ComponentInstancePath,
387}
388
389impl StoreReadSession<'_> {
390 pub fn read(&mut self, store: &StoreId, field: &str) -> Result<UiValue, StoreError> {
396 self.registry.read_tracked(self.reader, store, field)
397 }
398
399 pub fn read_path(
405 &mut self,
406 store: &StoreId,
407 field: &str,
408 path: &UiValuePath,
409 ) -> Result<UiValue, StoreError> {
410 self.registry
411 .read_path_tracked(self.reader, store, field, path)
412 }
413}
414
415#[derive(Clone, Debug, Error, Eq, PartialEq)]
416pub enum StoreError {
417 #[error("store `{0:?}` is already declared")]
418 DuplicateStore(StoreId),
419 #[error("store `{0:?}` is not declared")]
420 UnknownStore(StoreId),
421 #[error("field `{field}` is not declared in store `{store:?}`")]
422 UnknownField { store: StoreId, field: String },
423 #[error("value for `{store:?}.{field}` is invalid: {source}")]
424 InvalidValue {
425 store: StoreId,
426 field: String,
427 source: SchemaValidationError,
428 },
429 #[error(transparent)]
430 State(#[from] crate::StateError),
431 #[error(transparent)]
432 Path(#[from] UiValuePathError),
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::{StateField, ValueSchema};
439
440 fn session_schema() -> ComponentStateSchema {
441 ComponentStateSchema::new(BTreeMap::from([
442 (
443 "project".to_owned(),
444 StateField::new(ValueSchema::string(), UiValue::String("alpha".to_owned())),
445 ),
446 (
447 "sidebar_open".to_owned(),
448 StateField::new(ValueSchema::Bool, UiValue::Bool(true)),
449 ),
450 ]))
451 .unwrap()
452 }
453
454 fn nested_schema() -> ComponentStateSchema {
455 let row = |id: &str, label: &str| {
456 UiValue::Map(BTreeMap::from([
457 ("id".to_owned(), UiValue::String(id.to_owned())),
458 ("label".to_owned(), UiValue::String(label.to_owned())),
459 ]))
460 };
461 ComponentStateSchema::new(BTreeMap::from([(
462 "data".to_owned(),
463 StateField::new(
464 ValueSchema::UiValue,
465 UiValue::Map(BTreeMap::from([
466 (
467 "profile".to_owned(),
468 UiValue::Map(BTreeMap::from([
469 ("name".to_owned(), UiValue::String("Ada".to_owned())),
470 ("avatar".to_owned(), UiValue::String("ada.png".to_owned())),
471 ])),
472 ),
473 (
474 "rows".to_owned(),
475 UiValue::Array(vec![row("alpha", "Alpha"), row("beta", "Beta")]),
476 ),
477 ])),
478 ),
479 )]))
480 .unwrap()
481 }
482
483 #[test]
484 fn writes_invalidate_only_exact_field_readers() {
485 let store_id = StoreId::app("session");
486 let project = ComponentInstancePath::root("ProjectView", "project");
487 let sidebar = ComponentInstancePath::root("Sidebar", "sidebar");
488 let mut stores = StoreRegistry::new();
489 stores.declare(store_id.clone(), session_schema()).unwrap();
490
491 stores
492 .begin_read(&project)
493 .read(&store_id, "project")
494 .unwrap();
495 stores
496 .begin_read(&sidebar)
497 .read(&store_id, "sidebar_open")
498 .unwrap();
499
500 let invalidated = stores
501 .write(&store_id, "project", UiValue::String("beta".to_owned()))
502 .unwrap();
503 assert_eq!(invalidated, BTreeSet::from([project]));
504 }
505
506 #[test]
507 fn deserialized_schema_defaults_are_validated_before_store_declaration() {
508 let schema: ComponentStateSchema = serde_json::from_str(
509 r#"{"fields":{"count":{"schema":{"type":"integer"},"default":{"type":"string","value":"wrong"}}}}"#,
510 )
511 .unwrap();
512 let mut stores = StoreRegistry::new();
513 assert!(matches!(
514 stores.declare(StoreId::app("invalid"), schema),
515 Err(StoreError::State(crate::StateError::InvalidDefault { .. }))
516 ));
517 }
518
519 #[test]
520 fn snapshots_share_large_store_values_until_a_write() {
521 let id = StoreId::app("session");
522 let mut stores = StoreRegistry::new();
523 stores.declare(id.clone(), session_schema()).unwrap();
524 let snapshot = stores.clone();
525 assert!(Rc::ptr_eq(&stores.stores, &snapshot.stores));
526 stores
527 .write(&id, "sidebar_open", UiValue::Bool(false))
528 .unwrap();
529 assert!(!Rc::ptr_eq(&stores.stores, &snapshot.stores));
530 assert_eq!(
531 snapshot.stores[&id].values["sidebar_open"],
532 UiValue::Bool(true)
533 );
534 }
535
536 #[test]
537 fn repeated_value_does_not_invalidate() {
538 let store_id = StoreId::app("session");
539 let reader = ComponentInstancePath::root("App", "root");
540 let mut stores = StoreRegistry::new();
541 stores.declare(store_id.clone(), session_schema()).unwrap();
542 stores
543 .begin_read(&reader)
544 .read(&store_id, "project")
545 .unwrap();
546
547 assert!(
548 stores
549 .write(&store_id, "project", UiValue::String("alpha".to_owned()),)
550 .unwrap()
551 .is_empty()
552 );
553 }
554
555 #[test]
556 fn removing_window_clears_window_store() {
557 let store_id = StoreId::window("settings", "draft");
558 let mut stores = StoreRegistry::new();
559 stores.declare(store_id.clone(), session_schema()).unwrap();
560 stores.remove_window("settings");
561 assert!(matches!(
562 stores.write(&store_id, "sidebar_open", UiValue::Bool(false)),
563 Err(StoreError::UnknownStore(_))
564 ));
565 }
566
567 #[test]
568 fn successful_subtree_reconcile_removes_stale_app_store_readers() {
569 let id = StoreId::app("shared");
570 let root = ComponentInstancePath::root("App", "main");
571 let removed = root.child("Panel", "removed");
572 let mut stores = StoreRegistry::new();
573 stores.declare(id.clone(), session_schema()).unwrap();
574 stores.read_tracked(&removed, &id, "sidebar_open").unwrap();
575 stores.retain_reader_scope(&root, &BTreeSet::new());
576 assert!(
577 stores
578 .write(&id, "sidebar_open", UiValue::Bool(false))
579 .unwrap()
580 .is_empty()
581 );
582 }
583
584 #[test]
585 fn nested_writes_invalidate_only_changed_path_and_whole_readers() {
586 let store_id = StoreId::app("model");
587 let whole = ComponentInstancePath::root("Whole", "whole");
588 let name = ComponentInstancePath::root("Name", "name");
589 let avatar = ComponentInstancePath::root("Avatar", "avatar");
590 let mut stores = StoreRegistry::new();
591 stores.declare(store_id.clone(), nested_schema()).unwrap();
592 let name_path = UiValuePath::new(vec![
593 crate::UiValuePathSegment::Key("profile".to_owned()),
594 crate::UiValuePathSegment::Key("name".to_owned()),
595 ])
596 .unwrap();
597 let avatar_path = UiValuePath::new(vec![
598 crate::UiValuePathSegment::Key("profile".to_owned()),
599 crate::UiValuePathSegment::Key("avatar".to_owned()),
600 ])
601 .unwrap();
602 stores.read_tracked(&whole, &store_id, "data").unwrap();
603 stores
604 .read_path_tracked(&name, &store_id, "data", &name_path)
605 .unwrap();
606 stores
607 .read_path_tracked(&avatar, &store_id, "data", &avatar_path)
608 .unwrap();
609
610 assert_eq!(
611 stores
612 .write_path(
613 &store_id,
614 "data",
615 &name_path,
616 UiValue::String("Grace".to_owned()),
617 )
618 .unwrap(),
619 BTreeSet::from([whole, name])
620 );
621 assert!(
622 stores
623 .write_path(
624 &store_id,
625 "data",
626 &name_path,
627 UiValue::String("Grace".to_owned()),
628 )
629 .unwrap()
630 .is_empty()
631 );
632 }
633
634 #[test]
635 fn keyed_item_reader_ignores_reorder_but_observes_item_change() {
636 let store_id = StoreId::app("model");
637 let reader = ComponentInstancePath::root("Row", "beta");
638 let whole = ComponentInstancePath::root("Whole", "whole");
639 let mut stores = StoreRegistry::new();
640 stores.declare(store_id.clone(), nested_schema()).unwrap();
641 let row_path = UiValuePath::new(vec![
642 crate::UiValuePathSegment::Key("rows".to_owned()),
643 crate::UiValuePathSegment::Item {
644 key_field: "id".to_owned(),
645 key: "beta".to_owned(),
646 },
647 crate::UiValuePathSegment::Key("label".to_owned()),
648 ])
649 .unwrap();
650 stores
651 .read_path_tracked(&reader, &store_id, "data", &row_path)
652 .unwrap();
653 stores.read_tracked(&whole, &store_id, "data").unwrap();
654
655 let mut reordered = stores
656 .stores
657 .get(&store_id)
658 .unwrap()
659 .values
660 .get("data")
661 .unwrap()
662 .clone();
663 if let UiValue::Map(root) = &mut reordered
664 && let Some(UiValue::Array(rows)) = root.get_mut("rows")
665 {
666 rows.reverse();
667 }
668 assert_eq!(
669 stores.write(&store_id, "data", reordered).unwrap(),
670 BTreeSet::from([whole.clone()])
671 );
672 assert_eq!(
673 stores
674 .write_path(
675 &store_id,
676 "data",
677 &row_path,
678 UiValue::String("Updated".to_owned()),
679 )
680 .unwrap(),
681 BTreeSet::from([whole, reader])
682 );
683 }
684}