Skip to main content

gpui_rhai/
store.rs

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    /// Declare an application- or window-scoped typed store.
69    ///
70    /// # Errors
71    ///
72    /// Returns [`StoreError::DuplicateStore`] when the ID already exists.
73    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    /// Begin tracking reads for one component render.
94    ///
95    /// Previous subscriptions for the component are removed first so
96    /// conditional reads cannot leave stale invalidation edges.
97    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    /// Remove stale reader edges inside one successfully reconciled subtree.
117    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    /// Remove stale reader edges inside one successfully reconciled subtree.
128    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    /// Read a field and add one dependency without clearing earlier reads.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`StoreError::UnknownStore`] or [`StoreError::UnknownField`].
145    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    /// Read a field, subscribing `reader` when there is one.
158    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    /// Read one nested path and subscribe only to that value.
189    ///
190    /// # Errors
191    ///
192    /// Returns store/field errors or a precise [`UiValuePathError`].
193    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    /// Read one nested path, subscribing `reader` to it when there is one.
208    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    /// Validate and update a store field, returning only subscribed components.
241    ///
242    /// # Errors
243    ///
244    /// Returns [`StoreError::UnknownStore`], [`StoreError::UnknownField`], or
245    /// [`StoreError::InvalidValue`].
246    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    /// Replace one existing nested store path and return affected readers.
303    ///
304    /// # Errors
305    ///
306    /// Returns store/field/schema errors or a precise path error.
307    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    /// Remove a window and all stores/read dependencies scoped to it.
330    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    /// Read a field and subscribe the current component to that exact field.
391    ///
392    /// # Errors
393    ///
394    /// Returns [`StoreError::UnknownStore`] or [`StoreError::UnknownField`].
395    pub fn read(&mut self, store: &StoreId, field: &str) -> Result<UiValue, StoreError> {
396        self.registry.read_tracked(self.reader, store, field)
397    }
398
399    /// Read and subscribe to one exact nested path.
400    ///
401    /// # Errors
402    ///
403    /// Returns store/field or path errors.
404    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}