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            &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    /// Read one nested path and subscribe only to that value.
186    ///
187    /// # Errors
188    ///
189    /// Returns store/field errors or a precise [`UiValuePathError`].
190    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    /// Validate and update a store field, returning only subscribed components.
235    ///
236    /// # Errors
237    ///
238    /// Returns [`StoreError::UnknownStore`], [`StoreError::UnknownField`], or
239    /// [`StoreError::InvalidValue`].
240    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    /// Replace one existing nested store path and return affected readers.
297    ///
298    /// # Errors
299    ///
300    /// Returns store/field/schema errors or a precise path error.
301    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    /// Remove a window and all stores/read dependencies scoped to it.
324    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    /// Read a field and subscribe the current component to that exact field.
385    ///
386    /// # Errors
387    ///
388    /// Returns [`StoreError::UnknownStore`] or [`StoreError::UnknownField`].
389    pub fn read(&mut self, store: &StoreId, field: &str) -> Result<UiValue, StoreError> {
390        self.registry.read_tracked(self.reader, store, field)
391    }
392
393    /// Read and subscribe to one exact nested path.
394    ///
395    /// # Errors
396    ///
397    /// Returns store/field or path errors.
398    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}