Skip to main content

arete_server/view/
registry.rs

1use crate::row_scope::{ScopeField, DEFAULT_MAX_SCOPE_PARTITIONS};
2use crate::sorted_cache::{SortOrder, SortedViewCache};
3use crate::view::ViewSpec;
4use std::collections::HashMap;
5use std::sync::Arc;
6use tokio::sync::RwLock;
7
8#[derive(Clone)]
9pub struct ViewIndex {
10    by_export: HashMap<String, Vec<ViewSpec>>,
11    by_id: HashMap<String, ViewSpec>,
12    sorted_caches: Arc<RwLock<HashMap<String, SortedViewCache>>>,
13    /// Map from source view ID to derived view IDs
14    derived_by_source: HashMap<String, Vec<String>>,
15    /// Row-scoped entities (by export) and their scope fields.
16    entity_scopes: HashMap<String, ScopeField>,
17    /// Cap on the scope partitions of each sorted view of a scoped entity.
18    max_scope_partitions: usize,
19}
20
21impl ViewIndex {
22    pub fn new() -> Self {
23        Self {
24            by_export: HashMap::new(),
25            by_id: HashMap::new(),
26            sorted_caches: Arc::new(RwLock::new(HashMap::new())),
27            derived_by_source: HashMap::new(),
28            entity_scopes: HashMap::new(),
29            max_scope_partitions: DEFAULT_MAX_SCOPE_PARTITIONS,
30        }
31    }
32
33    /// Scope an entity's rows by `field` (row scoping, see
34    /// [`crate::row_scope`]): every view of the entity, derived views
35    /// included, then delivers a row only to sessions whose row scopes cover
36    /// it, and its sorted views keep their rows per scope.
37    ///
38    /// Call while building the index, before it is shared.
39    pub fn set_entity_scope(&mut self, export: impl Into<String>, field: ScopeField) {
40        self.entity_scopes.insert(export.into(), field);
41        self.partition_scoped_caches();
42    }
43
44    /// Cap the scope partitions each sorted view of a scoped entity keeps
45    /// (default [`DEFAULT_MAX_SCOPE_PARTITIONS`]). See
46    /// [`SortedViewCache::partition_by_scope`].
47    pub fn set_max_scope_partitions(&mut self, max_partitions: usize) {
48        self.max_scope_partitions = max_partitions.max(1);
49        self.partition_scoped_caches();
50    }
51
52    pub fn max_scope_partitions(&self) -> usize {
53        self.max_scope_partitions
54    }
55
56    /// The scope field governing a view's rows: its entity's, for the view
57    /// or any view it derives from. `None` for views of unscoped entities.
58    pub fn scope_field_for_view(&self, view_id: &str) -> Option<&ScopeField> {
59        if self.entity_scopes.is_empty() {
60            return None;
61        }
62        let mut current = view_id;
63        // Bounded walk: a source chain never revisits a view, and a cycle
64        // must not hang a subscription.
65        for _ in 0..=self.by_id.len() {
66            let Some(spec) = self.by_id.get(current) else {
67                // An unregistered source: judge by its entity prefix.
68                let entity = current.split('/').next().unwrap_or(current);
69                return self.entity_scopes.get(entity);
70            };
71            if let Some(field) = self.entity_scopes.get(&spec.export) {
72                return Some(field);
73            }
74            match spec.source_view.as_deref() {
75                Some(source) if source != current => current = source,
76                _ => return None,
77            }
78        }
79        None
80    }
81
82    /// Reject projections that would erase the field enforcing row access.
83    pub(crate) fn validate_scope_projection(&self, spec: &ViewSpec) -> anyhow::Result<()> {
84        if let (Some(scope), Some(fields)) =
85            (self.scope_field_for_view(&spec.id), &spec.projection.fields)
86        {
87            if !scope
88                .path()
89                .first()
90                .is_some_and(|root| fields.contains(root))
91            {
92                anyhow::bail!(
93                    "View {} projection must retain its row scope field",
94                    spec.id
95                );
96            }
97        }
98        Ok(())
99    }
100
101    pub(crate) fn validate_scope_projections(&self) -> anyhow::Result<()> {
102        for spec in self.by_id.values() {
103            self.validate_scope_projection(spec)?;
104        }
105        Ok(())
106    }
107
108    /// Whether any entity is row-scoped.
109    pub fn has_scoped_entities(&self) -> bool {
110        !self.entity_scopes.is_empty()
111    }
112
113    /// Partition the sorted caches of every view of a scoped entity.
114    fn partition_scoped_caches(&mut self) {
115        let scoped: Vec<(String, ScopeField)> = self
116            .by_id
117            .keys()
118            .filter_map(|id| {
119                self.scope_field_for_view(id)
120                    .map(|field| (id.clone(), field.clone()))
121            })
122            .collect();
123        if scoped.is_empty() {
124            return;
125        }
126        let max_partitions = self.max_scope_partitions;
127        let caches = Arc::get_mut(&mut self.sorted_caches)
128            .expect("Cannot partition sorted caches: Arc is shared");
129        let caches = caches.get_mut();
130        for (view_id, field) in scoped {
131            if let Some(cache) = caches.get_mut(&view_id) {
132                cache.partition_by_scope(field, max_partitions);
133            }
134        }
135    }
136
137    pub fn add_spec(&mut self, spec: ViewSpec) {
138        if let Some(ref source) = spec.source_view {
139            self.derived_by_source
140                .entry(source.clone())
141                .or_default()
142                .push(spec.id.clone());
143        }
144
145        if let Some(ref pipeline) = spec.pipeline {
146            if let Some(ref sort_config) = pipeline.sort {
147                self.init_sorted_cache_sync(
148                    &spec.id,
149                    sort_config.field_path.clone(),
150                    sort_config.order.into(),
151                );
152            }
153        }
154
155        // Only add non-derived views to by_export.
156        // Derived views receive updates via their source_view subscription,
157        // not directly from the projector.
158        if !spec.is_derived() {
159            self.by_export
160                .entry(spec.export.clone())
161                .or_default()
162                .push(spec.clone());
163        }
164        let partition = self.has_scoped_entities();
165        self.by_id.insert(spec.id.clone(), spec);
166        if partition {
167            self.partition_scoped_caches();
168        }
169    }
170
171    pub fn by_export(&self, entity: &str) -> &[ViewSpec] {
172        self.by_export
173            .get(entity)
174            .map(|v| v.as_slice())
175            .unwrap_or(&[])
176    }
177
178    pub fn get_view(&self, id: &str) -> Option<&ViewSpec> {
179        self.by_id.get(id)
180    }
181
182    /// Stable view order for snapshot compatibility contracts.
183    pub(crate) fn snapshot_specs(&self) -> Vec<&ViewSpec> {
184        let mut specs = self.by_id.values().collect::<Vec<_>>();
185        specs.sort_by(|left, right| left.id.cmp(&right.id));
186        specs
187    }
188
189    pub fn get_derived_views(&self) -> Vec<&ViewSpec> {
190        self.by_id.values().filter(|s| s.is_derived()).collect()
191    }
192
193    pub fn get_derived_views_for_source(&self, source_view_id: &str) -> Vec<&ViewSpec> {
194        self.derived_by_source
195            .get(source_view_id)
196            .map(|ids| ids.iter().filter_map(|id| self.by_id.get(id)).collect())
197            .unwrap_or_default()
198    }
199
200    pub fn sorted_caches(&self) -> Arc<RwLock<HashMap<String, SortedViewCache>>> {
201        self.sorted_caches.clone()
202    }
203
204    pub async fn init_sorted_cache(
205        &self,
206        view_id: &str,
207        sort_field: Vec<String>,
208        order: SortOrder,
209    ) {
210        let mut caches = self.sorted_caches.write().await;
211        if !caches.contains_key(view_id) {
212            caches.insert(
213                view_id.to_string(),
214                SortedViewCache::new(view_id.to_string(), sort_field, order),
215            );
216        }
217    }
218
219    fn init_sorted_cache_sync(&mut self, view_id: &str, sort_field: Vec<String>, order: SortOrder) {
220        let cache = SortedViewCache::new(view_id.to_string(), sort_field, order);
221        let caches = Arc::get_mut(&mut self.sorted_caches)
222            .expect("Cannot initialize sorted cache: Arc is shared");
223        let caches = caches.get_mut();
224        if !caches.contains_key(view_id) {
225            caches.insert(view_id.to_string(), cache);
226        }
227    }
228}
229
230impl Default for ViewIndex {
231    fn default() -> Self {
232        Self::new()
233    }
234}