1use std::{
4 any::Any,
5 collections::{HashMap, HashSet, VecDeque},
6 sync::{
7 Arc, Mutex, OnceLock, Weak,
8 atomic::{AtomicBool, AtomicU64, Ordering},
9 },
10};
11
12use dashmap::DashMap;
13use hyphae::{MapDiff, SelectExt, Signal, SubscriptionGuard, Watchable};
14use serde::de::DeserializeOwned;
15use serde_json::Value;
16use uuid::Uuid;
17
18use super::{
19 super::item::Eventable,
20 cell::FilteredCellMap,
21 context::{QueryBuildContext, QueryContext},
22 filter::{
23 BelongsToRoute, CompoundFkExtractor, CompoundKey, ID_ROUTE_FIELD_NAMES, LiveFilterQuery,
24 QueryRoute,
25 },
26 request::QueryRequest,
27 traits::{
28 AnyQuery, QueryBuildArgs, QueryHandler, QueryParams, QueryTestContext, QueryWindowBuildArgs,
29 },
30};
31use crate::{
32 common::with_id::WithId, core::item::downcast_any_item_arc, request::RequestContext,
33 server::MykoServerContext, store::StoreRegistry,
34};
35
36pub type QueryParseFn = fn(Value) -> Result<Arc<dyn AnyQuery>, anyhow::Error>;
42
43pub type QueryCellFactory = fn(
46 Arc<dyn AnyQuery>,
47 Arc<StoreRegistry>,
48 Arc<RequestContext>,
49 Option<Arc<MykoServerContext>>,
50) -> Result<FilteredCellMap, String>;
51
52pub type QueryWindowCellFactory = fn(
55 Arc<dyn AnyQuery>,
56 Arc<StoreRegistry>,
57 Arc<RequestContext>,
58 Arc<MykoServerContext>,
59 crate::wire::QueryWindow,
60) -> Result<Option<super::WindowedQuerySource>, String>;
61
62type AnyItemArc = Arc<dyn crate::core::item::AnyItem>;
63type AnyItemMap = hyphae::CellMap<Arc<str>, AnyItemArc>;
64type WeakAnyItemMap = hyphae::WeakCellMap<Arc<str>, AnyItemArc>;
65type BucketEntries = Vec<(Arc<str>, AnyItemArc)>;
66type BucketDiff = MapDiff<Arc<str>, AnyItemArc>;
67type BucketDiffs = Vec<BucketDiff>;
68type BucketAction = (AnyItemMap, BucketDiff);
69
70#[derive(Default)]
71struct BelongsToMutationState {
72 pending: VecDeque<Vec<BucketAction>>,
73 dispatching: bool,
74}
75
76type BucketSourceFn = Box<dyn Fn(&CompoundKey) -> FilteredCellMap>;
79
80inventory::collect!(QueryRegistration);
85
86#[derive(Debug, Clone, Copy, Default)]
87pub struct QueryRuntimeMetrics {
88 pub cell_factories_created: u64,
89 pub per_item_guards_created: u64,
90 pub per_item_guards_removed: u64,
91}
92
93#[derive(Debug, Clone, Default)]
94pub struct QueryRuntimePerIdMetrics {
95 pub query_id: Arc<str>,
96 pub cell_factories_created: u64,
97 pub per_item_guards_created: u64,
98 pub per_item_guards_removed: u64,
99}
100
101static QUERY_CELL_FACTORIES_CREATED: AtomicU64 = AtomicU64::new(0);
102static QUERY_PER_ITEM_GUARDS_CREATED: AtomicU64 = AtomicU64::new(0);
103static QUERY_PER_ITEM_GUARDS_REMOVED: AtomicU64 = AtomicU64::new(0);
104static QUERY_FACTORIES_BY_ID: OnceLock<DashMap<Arc<str>, u64>> = OnceLock::new();
105static QUERY_GUARDS_CREATED_BY_ID: OnceLock<DashMap<Arc<str>, u64>> = OnceLock::new();
106static QUERY_GUARDS_REMOVED_BY_ID: OnceLock<DashMap<Arc<str>, u64>> = OnceLock::new();
107static BELONGS_TO_SOURCE_INDEXES: OnceLock<DashMap<String, Weak<BelongsToSourceIndex>>> =
108 OnceLock::new();
109
110fn query_factories_by_id() -> &'static DashMap<Arc<str>, u64> {
111 QUERY_FACTORIES_BY_ID.get_or_init(DashMap::new)
112}
113
114fn query_guards_created_by_id() -> &'static DashMap<Arc<str>, u64> {
115 QUERY_GUARDS_CREATED_BY_ID.get_or_init(DashMap::new)
116}
117
118fn query_guards_removed_by_id() -> &'static DashMap<Arc<str>, u64> {
119 QUERY_GUARDS_REMOVED_BY_ID.get_or_init(DashMap::new)
120}
121
122fn belongs_to_source_indexes() -> &'static DashMap<String, Weak<BelongsToSourceIndex>> {
123 BELONGS_TO_SOURCE_INDEXES.get_or_init(DashMap::new)
124}
125
126fn increment_counter(map: &DashMap<Arc<str>, u64>, key: Arc<str>) {
127 if let Some(mut value) = map.get_mut(&key) {
128 *value = value.saturating_add(1);
129 } else {
130 map.insert(key, 1);
131 }
132}
133
134pub fn query_runtime_metrics() -> QueryRuntimeMetrics {
135 QueryRuntimeMetrics {
136 cell_factories_created: QUERY_CELL_FACTORIES_CREATED.load(Ordering::Relaxed),
137 per_item_guards_created: QUERY_PER_ITEM_GUARDS_CREATED.load(Ordering::Relaxed),
138 per_item_guards_removed: QUERY_PER_ITEM_GUARDS_REMOVED.load(Ordering::Relaxed),
139 }
140}
141
142#[must_use]
143pub fn query_runtime_metrics_by_id(limit: usize) -> Vec<QueryRuntimePerIdMetrics> {
144 let mut rows: Vec<QueryRuntimePerIdMetrics> = query_factories_by_id()
145 .iter()
146 .map(|entry| {
147 let query_id = entry.key().clone();
148 let cell_factories_created = *entry.value();
149 let per_item_guards_created = query_guards_created_by_id()
150 .get(&query_id)
151 .map_or(0, |v| *v.value());
152 let per_item_guards_removed = query_guards_removed_by_id()
153 .get(&query_id)
154 .map_or(0, |v| *v.value());
155 QueryRuntimePerIdMetrics {
156 query_id,
157 cell_factories_created,
158 per_item_guards_created,
159 per_item_guards_removed,
160 }
161 })
162 .collect();
163
164 rows.sort_by(|a, b| {
165 let a_live = a
166 .per_item_guards_created
167 .saturating_sub(a.per_item_guards_removed);
168 let b_live = b
169 .per_item_guards_created
170 .saturating_sub(b.per_item_guards_removed);
171 b_live
172 .cmp(&a_live)
173 .then_with(|| b.cell_factories_created.cmp(&a.cell_factories_created))
174 });
175 if rows.len() > limit {
176 rows.truncate(limit);
177 }
178 rows
179}
180
181struct BelongsToSourceIndex {
196 store: Arc<crate::store::EntityStore>,
197 buckets: DashMap<CompoundKey, WeakAnyItemMap>,
198 mutation_gate: Mutex<BelongsToMutationState>,
199 driver: Arc<AnyItemMap>,
200}
201
202impl BelongsToSourceIndex {
203 fn new(store: Arc<crate::store::EntityStore>, extract_fk: CompoundFkExtractor) -> Arc<Self> {
204 let driver = Arc::new(AnyItemMap::new());
205 let index = Arc::new(Self {
206 store,
207 buckets: DashMap::new(),
208 mutation_gate: Mutex::new(BelongsToMutationState::default()),
209 driver: driver.clone(),
210 });
211
212 let index_for_diffs = Arc::downgrade(&index);
213 let guard = index.store.subscribe_diffs(move |diff| {
214 if let Some(index) = index_for_diffs.upgrade() {
215 index.apply_diff(diff, extract_fk);
216 }
217 });
218 driver.own_guard(guard);
219 index
220 }
221
222 fn route_to_live_bucket(&self, key: &CompoundKey) -> Option<AnyItemMap> {
229 let entry = self.buckets.get(key)?;
230 if let Some(map) = entry.upgrade() {
231 return Some(map);
232 }
233 drop(entry);
234 self.buckets.remove(key);
235 None
236 }
237
238 fn bucket_for(
262 self: &Arc<Self>,
263 key: CompoundKey,
264 extract_fk: CompoundFkExtractor,
265 ) -> AnyItemMap {
266 let _mutation = self
271 .mutation_gate
272 .lock()
273 .unwrap_or_else(std::sync::PoisonError::into_inner);
274 match self.buckets.entry(key) {
275 dashmap::mapref::entry::Entry::Occupied(mut occupied) => {
276 if let Some(map) = occupied.get().upgrade() {
277 return map;
278 }
279 let map = Self::build_backfilled_bucket(&self.store, occupied.key(), extract_fk);
280 self.retain_for_bucket(&map);
281 occupied.insert(map.downgrade());
282 map
283 }
284 dashmap::mapref::entry::Entry::Vacant(vacant) => {
285 let map = Self::build_backfilled_bucket(&self.store, vacant.key(), extract_fk);
286 self.retain_for_bucket(&map);
287 vacant.insert(map.downgrade());
288 map
289 }
290 }
291 }
292
293 fn retain_for_bucket(self: &Arc<Self>, map: &AnyItemMap) {
298 let index = self.clone();
299 let guard = self.driver.subscribe_diffs(move |_| {
300 let _ = &index;
301 });
302 map.own_guard(guard);
303 }
304
305 fn build_backfilled_bucket(
306 store: &crate::store::EntityStore,
307 key: &CompoundKey,
308 extract_fk: CompoundFkExtractor,
309 ) -> AnyItemMap {
310 let map = AnyItemMap::new();
311 let backfill: BucketEntries = store
312 .snapshot()
313 .into_iter()
314 .filter(|(_, item)| extract_fk(item.as_any()).as_ref() == Some(key))
315 .collect();
316 if !backfill.is_empty() {
317 map.apply_diff_owned(MapDiff::Initial { entries: backfill });
318 }
319 map
320 }
321
322 fn sweep_dead_buckets(&self) {
330 self.buckets.retain(|_, weak| weak.upgrade().is_some());
331 }
332
333 fn apply_diff(&self, diff: &BucketDiff, extract_fk: CompoundFkExtractor) {
334 {
335 let mut mutation = self
336 .mutation_gate
337 .lock()
338 .unwrap_or_else(std::sync::PoisonError::into_inner);
339 let actions = self.prepare_diff_locked(diff, extract_fk);
340 mutation.pending.push_back(actions);
341 if mutation.dispatching {
342 return;
343 }
344 mutation.dispatching = true;
345 }
346
347 loop {
353 let actions = {
354 let mut mutation = self
355 .mutation_gate
356 .lock()
357 .unwrap_or_else(std::sync::PoisonError::into_inner);
358 if let Some(actions) = mutation.pending.pop_front() {
359 drop(mutation);
360 actions
361 } else {
362 mutation.dispatching = false;
363 drop(mutation);
364 return;
365 }
366 };
367 for (bucket, diff) in actions {
368 bucket.apply_diff_owned(diff);
369 }
370 }
371 }
372
373 fn prepare_diff_locked(
374 &self,
375 diff: &BucketDiff,
376 extract_fk: CompoundFkExtractor,
377 ) -> Vec<BucketAction> {
378 match diff {
379 MapDiff::Initial { entries } => self.prepare_initial(entries, extract_fk),
380 MapDiff::Insert { key, value } => extract_fk(value.as_any())
381 .and_then(|fk| {
382 self.route_action(
383 &fk,
384 MapDiff::Insert {
385 key: key.clone(),
386 value: value.clone(),
387 },
388 )
389 })
390 .into_iter()
391 .collect(),
392 MapDiff::Remove { key, old_value } => extract_fk(old_value.as_any())
393 .and_then(|fk| {
394 self.route_action(
395 &fk,
396 MapDiff::Remove {
397 key: key.clone(),
398 old_value: old_value.clone(),
399 },
400 )
401 })
402 .into_iter()
403 .collect(),
404 MapDiff::Update {
405 key,
406 old_value,
407 new_value,
408 } => self.prepare_update(key, old_value, new_value, extract_fk),
409 MapDiff::Batch { changes } => self.prepare_batch(changes, extract_fk),
410 }
411 }
412
413 fn route_action(&self, foreign_key: &CompoundKey, diff: BucketDiff) -> Option<BucketAction> {
414 self.route_to_live_bucket(foreign_key)
415 .map(|bucket| (bucket, diff))
416 }
417
418 fn prepare_initial(
419 &self,
420 entries: &BucketEntries,
421 extract_fk: CompoundFkExtractor,
422 ) -> Vec<BucketAction> {
423 let mut grouped: HashMap<CompoundKey, BucketEntries> = HashMap::new();
424 for (id, item) in entries {
425 if let Some(fk) = extract_fk(item.as_any()) {
426 grouped
427 .entry(fk)
428 .or_default()
429 .push((id.clone(), item.clone()));
430 }
431 }
432
433 self.buckets
434 .iter()
435 .filter(|entry| entry.value().upgrade().is_some())
436 .map(|entry| entry.key().clone())
437 .collect::<Vec<_>>()
438 .into_iter()
439 .filter_map(|key| {
440 self.route_action(
441 &key,
442 MapDiff::Initial {
443 entries: grouped.remove(&key).unwrap_or_default(),
444 },
445 )
446 })
447 .collect()
448 }
449
450 fn prepare_update(
451 &self,
452 key: &Arc<str>,
453 old_value: &AnyItemArc,
454 new_value: &AnyItemArc,
455 extract_fk: CompoundFkExtractor,
456 ) -> Vec<BucketAction> {
457 let old_fk = extract_fk(old_value.as_any());
458 let new_fk = extract_fk(new_value.as_any());
459 let mut actions = Vec::new();
460 match (old_fk, new_fk) {
461 (Some(old_fk), Some(new_fk)) if old_fk == new_fk => {
462 actions.extend(self.route_action(
463 &new_fk,
464 MapDiff::Update {
465 key: key.clone(),
466 old_value: old_value.clone(),
467 new_value: new_value.clone(),
468 },
469 ));
470 }
471 (old_fk, new_fk) => {
472 if let Some(old_fk) = old_fk {
473 actions.extend(self.route_action(
474 &old_fk,
475 MapDiff::Remove {
476 key: key.clone(),
477 old_value: old_value.clone(),
478 },
479 ));
480 }
481 if let Some(new_fk) = new_fk {
482 actions.extend(self.route_action(
483 &new_fk,
484 MapDiff::Insert {
485 key: key.clone(),
486 value: new_value.clone(),
487 },
488 ));
489 }
490 }
491 }
492 actions
493 }
494
495 fn group_change(
496 by_fk: &mut HashMap<CompoundKey, BucketDiffs>,
497 change: &BucketDiff,
498 extract_fk: CompoundFkExtractor,
499 ) {
500 let mut push = |foreign_key: Option<CompoundKey>, diff: BucketDiff| {
501 if let Some(foreign_key) = foreign_key {
502 by_fk.entry(foreign_key).or_default().push(diff);
503 }
504 };
505 match change {
506 MapDiff::Insert { key, value } => push(
507 extract_fk(value.as_any()),
508 MapDiff::Insert {
509 key: key.clone(),
510 value: value.clone(),
511 },
512 ),
513 MapDiff::Remove { key, old_value } => push(
514 extract_fk(old_value.as_any()),
515 MapDiff::Remove {
516 key: key.clone(),
517 old_value: old_value.clone(),
518 },
519 ),
520 MapDiff::Update {
521 key,
522 old_value,
523 new_value,
524 } => {
525 let old_fk = extract_fk(old_value.as_any());
526 let new_fk = extract_fk(new_value.as_any());
527 if old_fk == new_fk {
528 push(
529 new_fk,
530 MapDiff::Update {
531 key: key.clone(),
532 old_value: old_value.clone(),
533 new_value: new_value.clone(),
534 },
535 );
536 } else {
537 push(
538 old_fk,
539 MapDiff::Remove {
540 key: key.clone(),
541 old_value: old_value.clone(),
542 },
543 );
544 push(
545 new_fk,
546 MapDiff::Insert {
547 key: key.clone(),
548 value: new_value.clone(),
549 },
550 );
551 }
552 }
553 MapDiff::Initial { .. } | MapDiff::Batch { .. } => {}
554 }
555 }
556
557 fn prepare_batch(
558 &self,
559 changes: &BucketDiffs,
560 extract_fk: CompoundFkExtractor,
561 ) -> Vec<BucketAction> {
562 let mut actions = Vec::new();
563 let mut by_fk: HashMap<CompoundKey, BucketDiffs> = HashMap::new();
564 for change in changes {
565 if matches!(change, MapDiff::Initial { .. } | MapDiff::Batch { .. }) {
566 actions.extend(self.prepare_diff_locked(change, extract_fk));
567 } else {
568 Self::group_change(&mut by_fk, change, extract_fk);
569 }
570 }
571 actions.extend(by_fk.into_iter().filter_map(|(foreign_key, changes)| {
572 self.route_action(&foreign_key, MapDiff::Batch { changes })
573 }));
574 actions
575 }
576}
577
578pub fn build_belongs_to_source_map(
589 registry: Arc<StoreRegistry>,
590 host_id: Uuid,
591 local_type: &'static str,
592 field_names: &'static [&'static str],
593 extract_fk: CompoundFkExtractor,
594 foreign_ids: CompoundKey,
595) -> FilteredCellMap {
596 debug_assert_eq!(
597 field_names.len(),
598 foreign_ids.len(),
599 "build_belongs_to_source_map: field_names and foreign_ids must be positionally paired"
600 );
601 let index =
602 belongs_to_source_index_for(®istry, host_id, local_type, field_names, extract_fk);
603 drop(registry);
604 index.bucket_for(foreign_ids, extract_fk).lock()
605}
606
607fn belongs_to_source_index_for(
608 registry: &Arc<StoreRegistry>,
609 host_id: Uuid,
610 local_type: &'static str,
611 field_names: &'static [&'static str],
612 extract_fk: CompoundFkExtractor,
613) -> Arc<BelongsToSourceIndex> {
614 let key = format!("{host_id}:{local_type}:{}", field_names.join("+"));
615 match belongs_to_source_indexes().entry(key) {
616 dashmap::mapref::entry::Entry::Occupied(mut occupied) => {
617 if let Some(index) = occupied.get().upgrade() {
618 return index;
619 }
620 let store = registry.get_or_create(local_type);
621 let index = BelongsToSourceIndex::new(store, extract_fk);
622 occupied.insert(Arc::downgrade(&index));
623 index
624 }
625 dashmap::mapref::entry::Entry::Vacant(vacant) => {
626 let store = registry.get_or_create(local_type);
627 let index = BelongsToSourceIndex::new(store, extract_fk);
628 vacant.insert(Arc::downgrade(&index));
629 index
630 }
631 }
632}
633
634pub const UNION_KEYS_WARN_THRESHOLD: usize = 1000;
643
644fn additive_union_diff(
650 diff: &BucketDiff,
651 contribution: &mut HashMap<Arc<str>, AnyItemArc>,
652) -> Option<BucketDiff> {
653 match diff {
654 MapDiff::Initial { entries } => {
655 let mut changes = Vec::with_capacity(contribution.len().saturating_add(entries.len()));
656 for (key, old_value) in contribution.drain() {
657 changes.push(MapDiff::Remove { key, old_value });
658 }
659 for (key, value) in entries {
660 contribution.insert(key.clone(), value.clone());
661 changes.push(MapDiff::Insert {
662 key: key.clone(),
663 value: value.clone(),
664 });
665 }
666 (!changes.is_empty()).then_some(MapDiff::Batch { changes })
667 }
668 MapDiff::Batch { changes } => {
669 let rewritten: Vec<BucketDiff> = changes
670 .iter()
671 .filter_map(|change| additive_union_diff(change, contribution))
672 .collect();
673 if rewritten.is_empty() {
674 None
675 } else {
676 Some(MapDiff::Batch { changes: rewritten })
677 }
678 }
679 MapDiff::Insert { key, value } => {
680 contribution.insert(key.clone(), value.clone());
681 Some(diff.clone())
682 }
683 MapDiff::Remove { key, .. } => {
684 contribution.remove(key);
685 Some(diff.clone())
686 }
687 MapDiff::Update { key, new_value, .. } => {
688 contribution.insert(key.clone(), new_value.clone());
689 Some(diff.clone())
690 }
691 }
692}
693
694pub fn build_belongs_to_union_source_map(
711 registry: Arc<StoreRegistry>,
712 host_id: Uuid,
713 local_type: &'static str,
714 field_names: &'static [&'static str],
715 extract_fk: CompoundFkExtractor,
716 keys: Vec<CompoundKey>,
717) -> FilteredCellMap {
718 if keys.len() > UNION_KEYS_WARN_THRESHOLD {
719 tracing::warn!(
720 target: "myko::core::query::registration",
721 "belongs_to union route for {local_type}[{}] is subscribing to {} buckets \
722 (> {UNION_KEYS_WARN_THRESHOLD}) — likely a cartesian product of several \
723 large `In` fields on the same query",
724 field_names.join("+"),
725 keys.len(),
726 );
727 }
728
729 let index =
730 belongs_to_source_index_for(®istry, host_id, local_type, field_names, extract_fk);
731 let result: AnyItemMap = AnyItemMap::new();
732 for key in keys {
733 let bucket = index.bucket_for(key, extract_fk).lock();
734 let result_weak = result.downgrade();
735 let contribution = Mutex::new(HashMap::new());
736 let guard = bucket.subscribe_diffs(move |diff| {
737 let Some(result) = result_weak.upgrade() else {
738 return;
739 };
740 let additive = additive_union_diff(
747 diff,
748 &mut contribution
749 .lock()
750 .unwrap_or_else(std::sync::PoisonError::into_inner),
751 );
752 if let Some(additive) = additive {
753 result.apply_diff_owned(additive);
754 }
755 });
756 result.own(guard);
757 }
758 drop(registry);
759 result.lock()
760}
761
762#[must_use]
769pub fn cartesian_product(sets: Vec<Vec<Arc<str>>>) -> Vec<CompoundKey> {
770 sets.into_iter().fold(vec![CompoundKey::new()], |acc, set| {
771 acc.into_iter()
772 .flat_map(|prefix| {
773 set.iter().map(move |v| {
774 let mut next = prefix.clone();
775 next.push(v.clone());
776 next
777 })
778 })
779 .collect()
780 })
781}
782
783fn live_filter_matches<F: LiveFilterQuery>(filter: &F, item: &AnyItemArc) -> bool {
790 downcast_any_item_arc::<F::Item>(item, "query_live").is_some_and(|typed| filter.matches(&typed))
791}
792
793fn apply_bucket_candidates<F: LiveFilterQuery>(
805 result: &AnyItemMap,
806 filter: &F,
807 candidates: Vec<(Arc<str>, AnyItemArc)>,
808) {
809 for (id, item) in candidates {
810 if live_filter_matches(filter, &item) {
811 result.insert(id, item);
812 } else {
813 result.remove(&id);
814 }
815 }
816}
817
818fn reconcile_full_scope_membership<F: LiveFilterQuery>(
828 result: &AnyItemMap,
829 filter: &F,
830 candidates: Vec<(Arc<str>, AnyItemArc)>,
831) {
832 let mut current_ids: HashSet<Arc<str>> = HashSet::new();
835 result.for_each(|id, _| {
836 current_ids.insert(id.clone());
837 });
838 let mut seen: HashSet<Arc<str>> = HashSet::with_capacity(candidates.len());
839 for (id, item) in candidates {
840 seen.insert(id.clone());
841 let should_have = live_filter_matches(filter, &item);
842 let currently_has = current_ids.contains(&id);
843 match (should_have, currently_has) {
844 (true, false) => {
845 result.insert(id, item);
846 }
847 (false, true) => {
848 result.remove(&id);
849 }
850 _ => {}
851 }
852 }
853 for id in current_ids.difference(&seen) {
854 result.remove(id);
855 }
856}
857
858#[derive(Clone, Copy)]
864enum LiveDiffScope {
865 Bucket,
866 FullStore,
867}
868
869fn apply_live_diff<F: LiveFilterQuery>(
875 result: &AnyItemMap,
876 diff: &BucketDiff,
877 filter: &F,
878 scope: LiveDiffScope,
879) {
880 match diff {
881 MapDiff::Initial { entries } => match scope {
882 LiveDiffScope::Bucket => apply_bucket_candidates(result, filter, entries.clone()),
883 LiveDiffScope::FullStore => {
884 reconcile_full_scope_membership(result, filter, entries.clone());
885 }
886 },
887 MapDiff::Insert { key, value } => {
888 if live_filter_matches(filter, value) {
889 result.insert(key.clone(), value.clone());
890 }
891 }
892 MapDiff::Remove { key, .. } => {
893 result.remove(key);
894 }
895 MapDiff::Update { key, new_value, .. } => {
896 if live_filter_matches(filter, new_value) {
897 result.insert(key.clone(), new_value.clone());
898 } else {
899 result.remove(key);
900 }
901 }
902 MapDiff::Batch { changes } => {
903 for change in changes {
904 apply_live_diff(result, change, filter, scope);
905 }
906 }
907 }
908}
909
910struct LiveQueryState<F> {
915 bucket_guards: HashMap<CompoundKey, (FilteredCellMap, SubscriptionGuard)>,
916 bucket_filter_refs: HashMap<CompoundKey, Arc<Mutex<LiveFilterGeneration<F>>>>,
920 scan_guard: Option<SubscriptionGuard>,
921 scan_filter_ref: Option<Arc<Mutex<LiveFilterGeneration<F>>>>,
922 prev_route_field_names: Option<&'static [&'static str]>,
923}
924
925struct LiveFilterGeneration<F> {
926 generation: u64,
927 filter: F,
928}
929
930#[derive(Default)]
931struct LiveQuerySynchronization {
932 generation: AtomicU64,
933 reconciliation_gate: Mutex<()>,
934}
935
936impl<F> Default for LiveQueryState<F> {
937 fn default() -> Self {
938 Self {
939 bucket_guards: HashMap::new(),
940 bucket_filter_refs: HashMap::new(),
941 scan_guard: None,
942 scan_filter_ref: None,
943 prev_route_field_names: None,
944 }
945 }
946}
947
948pub fn query_live<F>(
964 registry: Arc<StoreRegistry>,
965 host_id: Uuid,
966 filter_cell: impl Watchable<F>,
967) -> FilteredCellMap
968where
969 F: LiveFilterQuery,
970{
971 let result: AnyItemMap = AnyItemMap::new();
972 let state: Arc<Mutex<LiveQueryState<F>>> = Arc::new(Mutex::new(LiveQueryState::default()));
973 let synchronization = Arc::new(LiveQuerySynchronization::default());
974
975 let result_weak = result.downgrade();
976 let guard = filter_cell.subscribe(move |signal| {
977 let Signal::Value(new_filter) = signal else {
978 return;
979 };
980 let Some(result) = result_weak.upgrade() else {
981 return;
982 };
983 let _reconciliation = synchronization
984 .reconciliation_gate
985 .lock()
986 .unwrap_or_else(std::sync::PoisonError::into_inner);
987 let mut state = state
988 .lock()
989 .unwrap_or_else(std::sync::PoisonError::into_inner);
990 let current_generation = synchronization
991 .generation
992 .fetch_add(1, Ordering::Relaxed)
993 .saturating_add(1);
994 reconcile_live_query(
995 ®istry,
996 host_id,
997 &result,
998 &mut state,
999 new_filter.as_ref(),
1000 current_generation,
1001 &synchronization,
1002 );
1003 });
1004 drop(filter_cell);
1005 result.own(guard);
1006 result.lock()
1007}
1008
1009fn reconcile_live_query<F: LiveFilterQuery>(
1010 registry: &Arc<StoreRegistry>,
1011 host_id: Uuid,
1012 result: &AnyItemMap,
1013 state: &mut LiveQueryState<F>,
1014 new_filter: &F,
1015 current_generation: u64,
1016 synchronization: &Arc<LiveQuerySynchronization>,
1017) {
1018 if let Some(route) = new_filter.query_route() {
1019 reconcile_indexed_live_query(
1020 registry,
1021 host_id,
1022 result,
1023 state,
1024 new_filter,
1025 (route, current_generation),
1026 synchronization,
1027 );
1028 } else {
1029 reconcile_scan_live_query(
1030 registry,
1031 result,
1032 state,
1033 new_filter,
1034 current_generation,
1035 synchronization,
1036 );
1037 }
1038}
1039
1040fn current_live_filter<F: LiveFilterQuery>(
1041 filter_state: &Mutex<LiveFilterGeneration<F>>,
1042 synchronization: &LiveQuerySynchronization,
1043) -> Option<F> {
1044 let filter_state = filter_state
1045 .lock()
1046 .unwrap_or_else(std::sync::PoisonError::into_inner);
1047 (filter_state.generation == synchronization.generation.load(Ordering::Relaxed))
1048 .then(|| filter_state.filter.clone())
1049}
1050
1051fn build_live_diff_callback<F: LiveFilterQuery>(
1052 result: &AnyItemMap,
1053 filter_state: Arc<Mutex<LiveFilterGeneration<F>>>,
1054 synchronization: &Arc<LiveQuerySynchronization>,
1055 scope: LiveDiffScope,
1056) -> impl Fn(&BucketDiff) + Send + Sync + 'static {
1057 let synchronization = synchronization.clone();
1058 let result = result.downgrade();
1059 let first = AtomicBool::new(true);
1060 move |diff| {
1061 let Some(result) = result.upgrade() else {
1062 return;
1063 };
1064 if first.swap(false, Ordering::Relaxed) {
1065 if let Some(filter) = current_live_filter(&filter_state, &synchronization) {
1066 apply_live_diff(&result, diff, &filter, scope);
1067 }
1068 return;
1069 }
1070 let _reconciliation = synchronization
1071 .reconciliation_gate
1072 .lock()
1073 .unwrap_or_else(std::sync::PoisonError::into_inner);
1074 let Some(filter) = current_live_filter(&filter_state, &synchronization) else {
1075 return;
1076 };
1077 apply_live_diff(&result, diff, &filter, scope);
1078 }
1079}
1080
1081fn clear_live_result(result: &AnyItemMap) {
1082 for (id, _) in result.snapshot() {
1083 result.remove(&id);
1084 }
1085}
1086
1087fn split_query_route(
1088 route: QueryRoute,
1089) -> (
1090 &'static [&'static str],
1091 HashSet<CompoundKey>,
1092 Option<CompoundFkExtractor>,
1093) {
1094 match route {
1095 QueryRoute::Ids(ids) => (
1096 ID_ROUTE_FIELD_NAMES,
1097 ids.into_iter()
1098 .map(|id| CompoundKey::from_iter([id]))
1099 .collect(),
1100 None,
1101 ),
1102 QueryRoute::BelongsTo(BelongsToRoute {
1103 field_names,
1104 keys,
1105 extract_fk,
1106 }) => (field_names, keys.into_iter().collect(), Some(extract_fk)),
1107 }
1108}
1109
1110fn build_live_bucket_source<F: LiveFilterQuery>(
1111 registry: &Arc<StoreRegistry>,
1112 host_id: Uuid,
1113 route_field_names: &'static [&'static str],
1114 extract_fk: Option<CompoundFkExtractor>,
1115) -> BucketSourceFn {
1116 extract_fk.map_or_else(
1117 || {
1118 let store = registry.get_or_create(F::entity_type());
1119 let make: BucketSourceFn = Box::new(move |key: &CompoundKey| {
1120 key.first().map_or_else(
1121 || AnyItemMap::new().lock(),
1122 |id| build_ids_source_map(&store, std::slice::from_ref(id)),
1123 )
1124 });
1125 make
1126 },
1127 |extract_fk| {
1128 let index = belongs_to_source_index_for(
1129 registry,
1130 host_id,
1131 F::entity_type(),
1132 route_field_names,
1133 extract_fk,
1134 );
1135 let make: BucketSourceFn =
1136 Box::new(move |key: &CompoundKey| index.bucket_for(key.clone(), extract_fk).lock());
1137 make
1138 },
1139 )
1140}
1141
1142fn reconcile_existing_buckets<F: LiveFilterQuery>(
1143 result: &AnyItemMap,
1144 state: &mut LiveQueryState<F>,
1145 new_filter: &F,
1146 new_keys: &HashSet<CompoundKey>,
1147 current_generation: u64,
1148) {
1149 for (key, (bucket, _guard)) in state
1150 .bucket_guards
1151 .extract_if(|key, _| !new_keys.contains(key))
1152 {
1153 for (id, _) in bucket.snapshot() {
1154 result.remove(&id);
1155 }
1156 state.bucket_filter_refs.remove(&key);
1157 }
1158 for key in new_keys {
1159 if let Some(filter_state) = state.bucket_filter_refs.get(key) {
1160 *filter_state
1161 .lock()
1162 .unwrap_or_else(std::sync::PoisonError::into_inner) = LiveFilterGeneration {
1163 generation: current_generation,
1164 filter: new_filter.clone(),
1165 };
1166 }
1167 if let Some((bucket, _)) = state.bucket_guards.get(key) {
1168 apply_bucket_candidates(result, new_filter, bucket.snapshot());
1169 }
1170 }
1171}
1172
1173fn add_live_buckets<F: LiveFilterQuery>(
1174 result: &AnyItemMap,
1175 state: &mut LiveQueryState<F>,
1176 new_filter: &F,
1177 new_keys: &HashSet<CompoundKey>,
1178 current_generation: u64,
1179 make_source: &BucketSourceFn,
1180 synchronization: &Arc<LiveQuerySynchronization>,
1181) {
1182 for key in new_keys {
1183 if state.bucket_guards.contains_key(key) {
1184 continue;
1185 }
1186 let bucket = make_source(key);
1187 apply_bucket_candidates(result, new_filter, bucket.snapshot());
1188 let filter_state = Arc::new(Mutex::new(LiveFilterGeneration {
1189 generation: current_generation,
1190 filter: new_filter.clone(),
1191 }));
1192 let guard = bucket.subscribe_diffs(build_live_diff_callback(
1193 result,
1194 filter_state.clone(),
1195 synchronization,
1196 LiveDiffScope::Bucket,
1197 ));
1198 state.bucket_guards.insert(key.clone(), (bucket, guard));
1199 state.bucket_filter_refs.insert(key.clone(), filter_state);
1200 }
1201}
1202
1203fn reconcile_indexed_live_query<F: LiveFilterQuery>(
1204 registry: &Arc<StoreRegistry>,
1205 host_id: Uuid,
1206 result: &AnyItemMap,
1207 state: &mut LiveQueryState<F>,
1208 new_filter: &F,
1209 route_generation: (QueryRoute, u64),
1210 synchronization: &Arc<LiveQuerySynchronization>,
1211) {
1212 state.scan_guard = None;
1213 let (route, current_generation) = route_generation;
1214 let (route_field_names, new_keys, extract_fk) = split_query_route(route);
1215 if state.prev_route_field_names != Some(route_field_names) {
1216 state.bucket_guards.clear();
1217 state.bucket_filter_refs.clear();
1218 clear_live_result(result);
1219 }
1220 reconcile_existing_buckets(result, state, new_filter, &new_keys, current_generation);
1221 if new_keys
1222 .iter()
1223 .any(|key| !state.bucket_guards.contains_key(key))
1224 {
1225 let make_source =
1226 build_live_bucket_source::<F>(registry, host_id, route_field_names, extract_fk);
1227 add_live_buckets(
1228 result,
1229 state,
1230 new_filter,
1231 &new_keys,
1232 current_generation,
1233 &make_source,
1234 synchronization,
1235 );
1236 }
1237 state.prev_route_field_names = Some(route_field_names);
1238}
1239
1240fn reconcile_scan_live_query<F: LiveFilterQuery>(
1241 registry: &Arc<StoreRegistry>,
1242 result: &AnyItemMap,
1243 state: &mut LiveQueryState<F>,
1244 new_filter: &F,
1245 current_generation: u64,
1246 synchronization: &Arc<LiveQuerySynchronization>,
1247) {
1248 if state.prev_route_field_names.is_some() {
1249 state.bucket_guards.clear();
1250 state.bucket_filter_refs.clear();
1251 clear_live_result(result);
1252 state.prev_route_field_names = None;
1253 }
1254 let store = registry.get_or_create(F::entity_type());
1255 reconcile_full_scope_membership(result, new_filter, store.snapshot());
1256 if state.scan_guard.is_none() {
1257 let filter_state = Arc::new(Mutex::new(LiveFilterGeneration {
1258 generation: current_generation,
1259 filter: new_filter.clone(),
1260 }));
1261 state.scan_filter_ref = Some(filter_state.clone());
1262 state.scan_guard = Some(store.subscribe_diffs(build_live_diff_callback(
1263 result,
1264 filter_state,
1265 synchronization,
1266 LiveDiffScope::FullStore,
1267 )));
1268 } else if let Some(filter_state) = &state.scan_filter_ref {
1269 *filter_state
1270 .lock()
1271 .unwrap_or_else(std::sync::PoisonError::into_inner) = LiveFilterGeneration {
1272 generation: current_generation,
1273 filter: new_filter.clone(),
1274 };
1275 }
1276}
1277
1278pub fn sweep_all_belongs_to_source_indexes() {
1285 belongs_to_source_indexes().retain(|_, weak| {
1286 let Some(index) = weak.upgrade() else {
1287 return false;
1288 };
1289 index.sweep_dead_buckets();
1290 true
1291 });
1292}
1293
1294#[must_use]
1303pub fn build_ids_source_map(
1304 store: &Arc<crate::store::EntityStore>,
1305 ids: &[Arc<str>],
1306) -> FilteredCellMap {
1307 use hyphae::{Materialize, Signal, Watchable};
1308
1309 let result: hyphae::CellMap<Arc<str>, AnyItemArc> = hyphae::CellMap::new();
1310 for id in ids {
1311 let key_cell = store.get(id).materialize();
1312 let result_weak = result.downgrade();
1320 let key_for_cb = id.clone();
1321 let guard = key_cell.subscribe(move |signal| {
1322 let Some(result_for_cb) = result_weak.upgrade() else {
1323 return;
1324 };
1325 if let Signal::Value(arc_opt) = signal {
1326 match arc_opt.as_ref() {
1327 Some(item) => {
1328 result_for_cb.insert(key_for_cb.clone(), item.clone());
1329 }
1330 None => {
1331 result_for_cb.remove(&key_for_cb);
1332 }
1333 }
1334 }
1335 });
1336 result.own(guard);
1337 }
1338 result.lock()
1339}
1340
1341pub fn filter_query_over_source<Q>(
1342 source: FilteredCellMap,
1343 query: Arc<Q>,
1344 query_context: Arc<QueryContext>,
1345) -> impl hyphae::MapQuery<Key = Arc<str>, Value = AnyItemArc>
1346where
1347 Q: QueryHandler + QueryParams + Clone + Send + Sync + 'static,
1348 Q::Item:
1349 DeserializeOwned + Eventable + WithId + Clone + std::fmt::Debug + Send + Sync + 'static,
1350{
1351 source.select(move |item_any: &AnyItemArc| {
1352 downcast_any_item_arc::<Q::Item>(item_any, "filter_query_over_source").is_some_and(|item| {
1353 Q::test_entity(QueryTestContext {
1354 item,
1355 query: query.clone(),
1356 query_context: query_context.clone(),
1357 })
1358 })
1359 })
1360}
1361
1362pub fn filter_typed_source<T, F>(
1370 source: FilteredCellMap,
1371 predicate: F,
1372) -> impl hyphae::MapQuery<Key = Arc<str>, Value = AnyItemArc>
1373where
1374 T: Eventable + WithId + Clone + std::fmt::Debug + Send + Sync + 'static,
1375 F: Fn(&Arc<T>) -> bool + Send + Sync + 'static,
1376{
1377 source.select(move |item_any: &AnyItemArc| {
1378 downcast_any_item_arc::<T>(item_any, "filter_typed_source")
1379 .is_some_and(|item| predicate(&item))
1380 })
1381}
1382
1383pub struct QueryRegistration {
1386 pub query_id: &'static str,
1388 pub query_item_type: &'static str,
1390 pub crate_name: &'static str,
1392 pub parse: QueryParseFn,
1394 pub cell_factory: QueryCellFactory,
1396 pub window_cell_factory: QueryWindowCellFactory,
1398 pub args: &'static [crate::reflection::OperationArgField],
1401 pub description: Option<&'static str>,
1403 pub include_in_typegen: bool,
1405}
1406
1407pub trait QueryFactory: QueryParams {
1416 fn parse(value: Value) -> Result<Arc<dyn AnyQuery>, anyhow::Error>;
1422
1423 fn cell_factory(
1429 query: Arc<dyn AnyQuery>,
1430 registry: Arc<StoreRegistry>,
1431 request_ctx: Arc<RequestContext>,
1432 server_ctx: Option<Arc<MykoServerContext>>,
1433 ) -> Result<FilteredCellMap, String>;
1434
1435 fn window_cell_factory(
1443 query: Arc<dyn AnyQuery>,
1444 registry: Arc<StoreRegistry>,
1445 request_ctx: Arc<RequestContext>,
1446 server_ctx: Arc<MykoServerContext>,
1447 window: crate::wire::QueryWindow,
1448 ) -> Result<Option<super::WindowedQuerySource>, String>;
1449}
1450
1451impl<Q: QueryParams> QueryFactory for Q
1452where
1453 Q::Item:
1454 Eventable + WithId + DeserializeOwned + Clone + std::fmt::Debug + Send + Sync + 'static,
1455{
1456 fn parse(value: Value) -> Result<Arc<dyn AnyQuery>, anyhow::Error> {
1457 let query = serde_json::from_value::<QueryRequest<Q>>(value)?;
1458 Ok(Arc::new(query))
1459 }
1460
1461 fn cell_factory(
1462 any_query: Arc<dyn AnyQuery>,
1463 registry: Arc<StoreRegistry>,
1464 request_ctx: Arc<RequestContext>,
1465 server_ctx: Option<Arc<MykoServerContext>>,
1466 ) -> Result<FilteredCellMap, String> {
1467 QUERY_CELL_FACTORIES_CREATED.fetch_add(1, Ordering::Relaxed);
1468 let query_id = Q::query_id_static();
1469 let _span = tracing::trace_span!("myko.query", query = query_id.as_ref()).entered();
1473 crate::server::dispatch_metrics::record_query(query_id.as_ref(), request_ctx.origin());
1474 increment_counter(query_factories_by_id(), query_id);
1475 let any_ref: &dyn Any = any_query.as_ref();
1476 let request: QueryRequest<Q> =
1477 crate::common::downcast::downcast_request(any_ref, "query payload")?;
1478 let query: Arc<Q> = Arc::new(request.query);
1479
1480 let query_ctx = Arc::new(QueryContext { req: request_ctx });
1481 let query_cell_ctx =
1482 QueryBuildContext::new(query_ctx.clone(), registry.clone(), server_ctx);
1483
1484 if let Some(built) = Q::build_view(QueryBuildArgs {
1485 query: query.clone(),
1486 query_context: query_cell_ctx,
1487 }) {
1488 return Ok(hyphae::MapQuery::materialize(built));
1489 }
1490
1491 let store: crate::store::EntityStore =
1492 (*registry.get_or_create(&Q::query_item_type_static())).clone();
1493 Ok(hyphae::MapQuery::materialize(store.select(
1494 move |item_any: &AnyItemArc| {
1495 downcast_any_item_arc::<Q::Item>(item_any, "QueryFactory::cell_factory")
1496 .is_some_and(|item| {
1497 Q::test_entity(QueryTestContext {
1498 item,
1499 query: query.clone(),
1500 query_context: query_ctx.clone(),
1501 })
1502 })
1503 },
1504 )))
1505 }
1506
1507 fn window_cell_factory(
1508 any_query: Arc<dyn AnyQuery>,
1509 registry: Arc<StoreRegistry>,
1510 request_ctx: Arc<RequestContext>,
1511 server_ctx: Arc<MykoServerContext>,
1512 window: crate::wire::QueryWindow,
1513 ) -> Result<Option<super::WindowedQuerySource>, String> {
1514 let any_ref: &dyn Any = any_query.as_ref();
1515 let request: QueryRequest<Q> =
1516 crate::common::downcast::downcast_request(any_ref, "windowed query payload")?;
1517 let query_context = Arc::new(QueryContext { req: request_ctx });
1518 Q::build_window(QueryWindowBuildArgs {
1519 query: Arc::new(request.query),
1520 query_context: QueryBuildContext::new(query_context, registry, Some(server_ctx)),
1521 window,
1522 })
1523 }
1524}
1525
1526#[cfg(test)]
1527mod belongs_to_source_index_tests {
1528 use std::any::Any;
1529
1530 use hyphae::{Gettable, Materialize};
1531 use serde::Serialize;
1532
1533 use super::*;
1534 use crate::common::with_id::WithId;
1535
1536 #[derive(Debug, Clone, PartialEq, Serialize)]
1537 struct TestChild {
1538 id: Arc<str>,
1539 parent_id: Arc<str>,
1540 }
1541
1542 impl WithId for TestChild {
1543 fn id(&self) -> Arc<str> {
1544 self.id.clone()
1545 }
1546 }
1547
1548 impl crate::core::item::AnyItem for TestChild {
1549 fn as_any(&self) -> &dyn Any {
1550 self
1551 }
1552
1553 fn entity_type(&self) -> &'static str {
1554 "TestChild"
1555 }
1556
1557 fn equals(&self, other: &dyn crate::core::item::AnyItem) -> bool {
1558 other
1559 .as_any()
1560 .downcast_ref::<Self>()
1561 .is_some_and(|t| t == self)
1562 }
1563 }
1564
1565 fn extract_parent_fk(item: &dyn Any) -> Option<CompoundKey> {
1566 item.downcast_ref::<TestChild>()
1567 .map(|c| smallvec::smallvec![c.parent_id.clone()])
1568 }
1569
1570 fn child(id: &str, parent: &str) -> (Arc<str>, AnyItemArc) {
1571 let item: AnyItemArc = Arc::new(TestChild {
1572 id: Arc::from(id),
1573 parent_id: Arc::from(parent),
1574 });
1575 (Arc::from(id), item)
1576 }
1577
1578 fn new_store() -> Arc<crate::store::EntityStore> {
1579 Arc::new(hyphae::CellMap::new())
1580 }
1581
1582 #[test]
1583 fn dropped_subscriptions_do_not_leak_across_many_distinct_parents() {
1584 let store = new_store();
1589 let index = BelongsToSourceIndex::new(store, extract_parent_fk);
1590
1591 for i in 0..50 {
1592 let parent: Arc<str> = Arc::from(format!("parent-{i}"));
1593 let bucket = index.bucket_for(smallvec::smallvec![parent], extract_parent_fk);
1594 drop(bucket); }
1596
1597 index.sweep_dead_buckets();
1598 assert_eq!(
1599 index.buckets.len(),
1600 0,
1601 "sweep must reap all buckets once every subscriber has dropped"
1602 );
1603 }
1604
1605 #[test]
1606 fn live_subscription_survives_going_empty_then_repopulating() {
1607 let store = new_store();
1613 let (id, item) = child("c1", "parent-x");
1614 store.insert(id.clone(), item);
1615
1616 let index = BelongsToSourceIndex::new(store.clone(), extract_parent_fk);
1617 let bucket = index.bucket_for(
1618 smallvec::smallvec![Arc::from("parent-x")],
1619 extract_parent_fk,
1620 );
1621 assert_eq!(bucket.snapshot().len(), 1);
1622
1623 store.remove_many(vec![id]);
1626 assert_eq!(bucket.snapshot().len(), 0);
1627
1628 let (id2, item2) = child("c2", "parent-x");
1631 store.insert(id2, item2);
1632 assert_eq!(
1633 bucket.snapshot().len(),
1634 1,
1635 "a live subscriber must see re-population after its bucket went empty"
1636 );
1637 }
1638
1639 #[test]
1640 fn resubscribing_after_reap_backfills_current_children() {
1641 let store = new_store();
1646 let (id, item) = child("c1", "parent-y");
1647 store.insert(id, item);
1648
1649 let index = BelongsToSourceIndex::new(store.clone(), extract_parent_fk);
1650
1651 {
1652 let bucket = index.bucket_for(
1653 smallvec::smallvec![Arc::from("parent-y")],
1654 extract_parent_fk,
1655 );
1656 assert_eq!(bucket.snapshot().len(), 1);
1657 }
1658 index.sweep_dead_buckets();
1659 assert!(index.buckets.is_empty());
1660
1661 let bucket = index.bucket_for(
1662 smallvec::smallvec![Arc::from("parent-y")],
1663 extract_parent_fk,
1664 );
1665 assert_eq!(
1666 bucket.snapshot().len(),
1667 1,
1668 "resubscribing after the bucket was reaped must backfill current children, not start empty"
1669 );
1670 }
1671
1672 #[test]
1673 fn concurrent_bucket_for_calls_for_the_same_key_never_orphan_a_bucket() {
1674 const N: usize = 16;
1675
1676 let store = new_store();
1692 let index = Arc::new(BelongsToSourceIndex::new(store.clone(), extract_parent_fk));
1693 let key: CompoundKey = smallvec::smallvec![Arc::from("parent-race")];
1694
1695 let barrier = Arc::new(std::sync::Barrier::new(N));
1696 let mut handles = Vec::with_capacity(N);
1697 for _ in 0..N {
1698 let index = index.clone();
1699 let key = key.clone();
1700 let barrier = barrier.clone();
1701 handles.push(std::thread::spawn(move || {
1702 barrier.wait();
1703 index.bucket_for(key, extract_parent_fk)
1704 }));
1705 }
1706 let buckets: Vec<AnyItemMap> = handles
1707 .into_iter()
1708 .filter_map(|handle| handle.join().ok())
1709 .collect();
1710 assert_eq!(buckets.len(), N, "all bucket threads must complete");
1711
1712 assert_eq!(
1713 index.buckets.len(),
1714 1,
1715 "N concurrent creators for the same key must converge on exactly one bucket entry"
1716 );
1717
1718 let (id, item) = child("c-race", "parent-race");
1722 store.insert(id, item);
1723 for (i, bucket) in buckets.iter().enumerate() {
1724 assert_eq!(
1725 bucket.snapshot().len(),
1726 1,
1727 "handle {i} must observe the post-race insert — an orphaned bucket stays empty forever"
1728 );
1729 }
1730 }
1731
1732 #[test]
1733 fn concurrent_first_subscription_and_insert_cannot_lose_the_insert() {
1734 for i in 0..128 {
1739 let store = new_store();
1740 let index = BelongsToSourceIndex::new(store.clone(), extract_parent_fk);
1741 let barrier = Arc::new(std::sync::Barrier::new(2));
1742
1743 let bucket_thread = {
1744 let index = index.clone();
1745 let barrier = barrier.clone();
1746 std::thread::spawn(move || {
1747 barrier.wait();
1748 index.bucket_for(
1749 smallvec::smallvec![Arc::from("parent-race")],
1750 extract_parent_fk,
1751 )
1752 })
1753 };
1754 let insert_thread = {
1755 let store = store.clone();
1756 let barrier = barrier.clone();
1757 std::thread::spawn(move || {
1758 barrier.wait();
1759 let (id, item) = child(&format!("child-{i}"), "parent-race");
1760 store.insert(id, item);
1761 })
1762 };
1763
1764 let bucket = bucket_thread.join();
1765 assert!(bucket.is_ok(), "bucket thread must complete");
1766 let Ok(bucket) = bucket else {
1767 return;
1768 };
1769 assert!(insert_thread.join().is_ok());
1770 assert_eq!(
1771 bucket.snapshot().len(),
1772 1,
1773 "iteration {i} lost the insert racing first bucket construction"
1774 );
1775 }
1776 }
1777
1778 #[test]
1779 fn bucket_diff_fanout_can_reenter_bucket_creation() {
1780 let store = new_store();
1781 let index = BelongsToSourceIndex::new(store.clone(), extract_parent_fk);
1782 let bucket = index.bucket_for(
1783 smallvec::smallvec![Arc::from("parent-source")],
1784 extract_parent_fk,
1785 );
1786 let (sent, received) = std::sync::mpsc::channel();
1787 let index_for_callback = index.clone();
1788 let guard = bucket.subscribe_diffs(move |_| {
1789 let nested = index_for_callback.bucket_for(
1793 smallvec::smallvec![Arc::from("parent-nested")],
1794 extract_parent_fk,
1795 );
1796 drop(nested);
1797 let _send_result = sent.send(());
1798 });
1799
1800 assert!(received.recv().is_ok());
1802 let insert = std::thread::spawn(move || {
1803 let (id, item) = child("child-reentrant", "parent-source");
1804 store.insert(id, item);
1805 });
1806 assert!(
1807 received
1808 .recv_timeout(std::time::Duration::from_secs(2))
1809 .is_ok(),
1810 "bucket fanout deadlocked while re-entering bucket_for"
1811 );
1812 assert!(insert.join().is_ok());
1813 drop(guard);
1814 }
1815
1816 #[test]
1817 fn global_registry_does_not_retain_an_unused_index() {
1818 let registry = Arc::new(crate::store::StoreRegistry::new());
1819 let host_id = Uuid::new_v4();
1820 let registry_key = format!("{host_id}:TestChild:parent_id");
1821 let index = belongs_to_source_index_for(
1822 ®istry,
1823 host_id,
1824 "TestChild",
1825 &["parent_id"],
1826 extract_parent_fk,
1827 );
1828 let weak = Arc::downgrade(&index);
1829 let bucket = index.bucket_for(
1830 smallvec::smallvec![Arc::from("parent-live")],
1831 extract_parent_fk,
1832 );
1833
1834 drop(index);
1835 assert!(
1836 weak.upgrade().is_some(),
1837 "a live bucket must retain the index that routes its updates"
1838 );
1839 drop(bucket);
1840 assert!(
1841 weak.upgrade().is_none(),
1842 "the global registry or driver subscription retained an unused index"
1843 );
1844
1845 sweep_all_belongs_to_source_indexes();
1846 assert!(
1847 !belongs_to_source_indexes().contains_key(®istry_key),
1848 "sweeping must remove the dead weak registry entry"
1849 );
1850 }
1851
1852 #[derive(Debug, Clone, PartialEq, Serialize)]
1860 struct TestCursor {
1861 id: Arc<str>,
1862 node_id: Arc<str>,
1863 session_id: Arc<str>,
1864 anchor_id: Arc<str>,
1865 }
1866
1867 impl WithId for TestCursor {
1868 fn id(&self) -> Arc<str> {
1869 self.id.clone()
1870 }
1871 }
1872
1873 impl crate::core::item::AnyItem for TestCursor {
1874 fn as_any(&self) -> &dyn Any {
1875 self
1876 }
1877
1878 fn entity_type(&self) -> &'static str {
1879 "TestCursor"
1880 }
1881
1882 fn equals(&self, other: &dyn crate::core::item::AnyItem) -> bool {
1883 other
1884 .as_any()
1885 .downcast_ref::<Self>()
1886 .is_some_and(|t| t == self)
1887 }
1888 }
1889
1890 fn cursor(id: &str, node: &str, session: &str) -> (Arc<str>, AnyItemArc) {
1891 cursor_with_anchor(id, node, session, "anchor-default")
1892 }
1893
1894 fn cursor_with_anchor(
1895 id: &str,
1896 node: &str,
1897 session: &str,
1898 anchor: &str,
1899 ) -> (Arc<str>, AnyItemArc) {
1900 let item: AnyItemArc = Arc::new(TestCursor {
1901 id: Arc::from(id),
1902 node_id: Arc::from(node),
1903 session_id: Arc::from(session),
1904 anchor_id: Arc::from(anchor),
1905 });
1906 (Arc::from(id), item)
1907 }
1908
1909 fn extract_node_and_session_fk(item: &dyn Any) -> Option<CompoundKey> {
1912 item.downcast_ref::<TestCursor>()
1913 .map(|c| smallvec::smallvec![c.node_id.clone(), c.session_id.clone()])
1914 }
1915
1916 #[test]
1917 fn compound_key_separates_watchers_sharing_one_field_but_not_the_other() {
1918 let store = new_store();
1925 let (id_a, item_a) = cursor("cursor-a", "node-A", "session-PROD");
1926 let (id_b, item_b) = cursor("cursor-b", "node-B", "session-PROD");
1927 store.insert(id_a, item_a);
1928 store.insert(id_b, item_b);
1929
1930 let index = BelongsToSourceIndex::new(store.clone(), extract_node_and_session_fk);
1931
1932 let key_a: CompoundKey =
1933 smallvec::smallvec![Arc::from("node-A"), Arc::from("session-PROD")];
1934 let key_b: CompoundKey =
1935 smallvec::smallvec![Arc::from("node-B"), Arc::from("session-PROD")];
1936
1937 let bucket_a = index.bucket_for(key_a, extract_node_and_session_fk);
1938 let bucket_b = index.bucket_for(key_b, extract_node_and_session_fk);
1939
1940 assert_eq!(
1941 bucket_a.snapshot().len(),
1942 1,
1943 "node-A's bucket sees only its own cursor"
1944 );
1945 assert_eq!(
1946 bucket_b.snapshot().len(),
1947 1,
1948 "node-B's bucket sees only its own cursor"
1949 );
1950 assert_eq!(
1951 index.buckets.len(),
1952 2,
1953 "distinct (node, session) pairs get distinct buckets"
1954 );
1955
1956 let (alpha_id, alpha_item) = cursor("cursor-a-tick2", "node-A", "session-PROD");
1960 store.insert(alpha_id, alpha_item);
1961 assert_eq!(
1962 bucket_a.snapshot().len(),
1963 2,
1964 "node-A's bucket must see its own new entry"
1965 );
1966 assert_eq!(
1967 bucket_b.snapshot().len(),
1968 1,
1969 "node-B's bucket must be unaffected by node-A's insert"
1970 );
1971
1972 let (bravo_id, bravo_item) = cursor("cursor-b-tick2", "node-B", "session-PROD");
1973 store.insert(bravo_id, bravo_item);
1974 assert_eq!(
1975 bucket_b.snapshot().len(),
1976 2,
1977 "node-B's bucket must independently see its own new entry — this is the \
1978 regression rship-qtu hit: the second-registered watcher never received \
1979 a diff under single-field session-only routing"
1980 );
1981 }
1982
1983 #[test]
1984 fn compound_and_single_field_routing_never_share_a_bucket() {
1985 let store = new_store();
1996 let index = BelongsToSourceIndex::new(store, extract_node_and_session_fk);
1997 let key: CompoundKey = smallvec::smallvec![Arc::from("node-A"), Arc::from("session-PROD")];
1998 let bucket = index.bucket_for(key.clone(), extract_node_and_session_fk);
1999 assert_eq!(bucket.snapshot().len(), 0);
2000 assert!(index.buckets.contains_key(&key));
2001 }
2002
2003 #[test]
2010 fn cartesian_product_expands_multi_field_value_sets() {
2011 let sets = vec![
2012 vec![Arc::from("node-A"), Arc::from("node-B")],
2013 vec![Arc::from("session-PROD")],
2014 ];
2015 let mut product = cartesian_product(sets);
2016 product.sort();
2017 assert_eq!(
2018 product,
2019 vec![
2020 CompoundKey::from_iter([Arc::<str>::from("node-A"), Arc::from("session-PROD")]),
2021 CompoundKey::from_iter([Arc::<str>::from("node-B"), Arc::from("session-PROD")]),
2022 ]
2023 );
2024 }
2025
2026 #[test]
2027 fn cartesian_product_empty_set_yields_no_keys() {
2028 let sets = vec![vec![Arc::from("node-A")], vec![]];
2031 assert!(cartesian_product(sets).is_empty());
2032 }
2033
2034 #[test]
2035 fn union_source_map_unions_k_buckets_and_stays_reactive() {
2036 let registry = Arc::new(crate::store::StoreRegistry::new());
2046 let store = registry.get_or_create("TestCursor");
2047 let (id_a, item_a) = cursor("cursor-a", "node-A", "session-PROD");
2048 let (id_b, item_b) = cursor("cursor-b", "node-B", "session-PROD");
2049 let (id_c, item_c) = cursor("cursor-c", "node-C", "session-PROD"); store.insert(id_a, item_a);
2051 store.insert(id_b, item_b);
2052 store.insert(id_c, item_c);
2053
2054 let host_id = Uuid::new_v4();
2055 let keys = vec![
2056 smallvec::smallvec![Arc::from("node-A"), Arc::from("session-PROD")],
2057 smallvec::smallvec![Arc::from("node-B"), Arc::from("session-PROD")],
2058 ];
2059 let union = build_belongs_to_union_source_map(
2060 registry.clone(),
2061 host_id,
2062 "TestCursor",
2063 &["node_id", "session_id"],
2064 extract_node_and_session_fk,
2065 keys,
2066 );
2067
2068 assert_eq!(
2069 union.snapshot().len(),
2070 2,
2071 "union must contain exactly node-A's and node-B's cursors, not node-C's"
2072 );
2073
2074 let (alpha_id, alpha_item) = cursor("cursor-a2", "node-A", "session-PROD");
2075 store.insert(alpha_id, alpha_item);
2076 assert_eq!(
2077 union.snapshot().len(),
2078 3,
2079 "union must keep tracking writes to any of its unioned buckets"
2080 );
2081
2082 let (charlie_id, charlie_item) = cursor("cursor-c2", "node-C", "session-PROD");
2083 store.insert(charlie_id, charlie_item);
2084 assert_eq!(
2085 union.snapshot().len(),
2086 3,
2087 "writes to a non-unioned bucket must never appear in the union"
2088 );
2089 }
2090
2091 #[test]
2092 fn compound_union_with_residual_filter_propagates_batch_delete() {
2093 let registry = Arc::new(crate::store::StoreRegistry::new());
2094 let store = registry.get_or_create("TestCursor");
2095 let (matched_id, matched) =
2096 cursor_with_anchor("cursor-match", "node-A", "session-PROD", "anchor-match");
2097 let (residual_miss_id, residual_miss) = cursor_with_anchor(
2098 "cursor-residual-miss",
2099 "node-A",
2100 "session-PROD",
2101 "anchor-other",
2102 );
2103 store.insert(matched_id.clone(), matched);
2104 store.insert(residual_miss_id, residual_miss);
2105
2106 let source = build_belongs_to_union_source_map(
2107 registry,
2108 Uuid::new_v4(),
2109 "TestCursor",
2110 &["node_id", "session_id"],
2111 extract_node_and_session_fk,
2112 vec![smallvec::smallvec![
2113 Arc::from("node-A"),
2114 Arc::from("session-PROD")
2115 ]],
2116 );
2117 let filtered = hyphae::MapQuery::materialize(source.select(|item| {
2118 item.as_any()
2119 .downcast_ref::<TestCursor>()
2120 .is_some_and(|cursor| cursor.anchor_id.as_ref() == "anchor-match")
2121 }));
2122 let items = filtered.items().materialize();
2123 assert_eq!(items.get().len(), 1);
2124
2125 store.remove_many(vec![matched_id]);
2128
2129 assert_eq!(items.get().len(), 0);
2130 }
2131
2132 #[test]
2133 fn union_source_map_empty_keys_yields_empty_reactive_map() {
2134 let registry = Arc::new(crate::store::StoreRegistry::new());
2135 let store = registry.get_or_create("TestCursor");
2136 let (id, item) = cursor("cursor-a", "node-A", "session-PROD");
2137 store.insert(id, item);
2138
2139 let union = build_belongs_to_union_source_map(
2140 registry,
2141 Uuid::new_v4(),
2142 "TestCursor",
2143 &["node_id", "session_id"],
2144 extract_node_and_session_fk,
2145 Vec::new(),
2146 );
2147 assert_eq!(union.snapshot().len(), 0);
2148 }
2149}