1use crate::mutation_batch::SlotIndexDomain;
8use crate::shared_entity::{
9 split_version, with_version, EntityFields, SharedEntity, VERSION_FIELD,
10};
11use arete_interpreter::AccountPosition;
12use lru::LruCache;
13use serde::{Deserialize, Serialize};
14use serde_json::Value;
15use std::collections::BTreeMap;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::sync::RwLock;
19
20const DEFAULT_MAX_ENTITIES_PER_VIEW: usize = 500;
21const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
22const DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE: usize = 50;
23const DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE: usize = 100;
24const EVICTED_KEYS_PER_CACHED_ENTITY: usize = 8;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum PatchOrigin {
43 Creation,
47 Change,
51 Unknown,
54}
55
56#[derive(Debug, Clone, PartialEq)]
58pub enum CacheWrite {
59 Merged,
61 Created,
64 Refused { patch: Value },
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum Admission {
75 Refuse,
76 Create,
77 Merge,
78}
79
80#[derive(Debug, Clone)]
83pub(crate) struct ViewVersion<'a> {
84 pub view_id: &'a str,
85 pub version: Option<Value>,
86}
87
88struct ViewEntries {
95 max_entities: usize,
96 entities: LruCache<String, SharedEntity>,
97 evicted: Option<LruCache<String, ()>>,
101 creations_marked: bool,
104 lifetimes: LruCache<String, EntityLifetimeCheckpoint>,
108}
109
110impl ViewEntries {
111 fn new(max_entities: usize) -> Self {
112 assert!(max_entities > 0, "max_entities_per_view must be > 0");
113 Self {
114 max_entities,
115 entities: LruCache::unbounded(),
116 evicted: None,
117 creations_marked: false,
118 lifetimes: LruCache::unbounded(),
119 }
120 }
121
122 fn mark_creations(&mut self) {
125 self.creations_marked = true;
126 self.evicted = None;
127 }
128
129 fn store(&mut self, key: String, entity: SharedEntity) {
131 self.forget_evicted(&key);
132 self.entities.put(key, entity);
133 while self.entities.len() > self.max_entities {
134 match self.entities.pop_lru() {
135 Some((evicted, _)) => self.remember_evicted(evicted),
136 None => break,
137 }
138 }
139 }
140
141 fn remember_evicted(&mut self, key: String) {
142 if self.creations_marked {
143 return;
144 }
145 let capacity = self
146 .max_entities
147 .saturating_mul(EVICTED_KEYS_PER_CACHED_ENTITY);
148 let evicted = self.evicted.get_or_insert_with(LruCache::unbounded);
149 evicted.put(key, ());
150 while evicted.len() > capacity {
151 evicted.pop_lru();
152 }
153 }
154
155 fn forget_evicted(&mut self, key: &str) {
156 if let Some(evicted) = self.evicted.as_mut() {
157 evicted.pop(key);
158 }
159 }
160
161 fn was_evicted(&mut self, key: &str) -> bool {
164 self.evicted
165 .as_mut()
166 .is_some_and(|evicted| evicted.get(key).is_some())
167 }
168
169 fn accepts(
170 &self,
171 key: &str,
172 creation: bool,
173 account_position: Option<AccountPosition>,
174 source_seq: Option<&str>,
175 source_domain: Option<SlotIndexDomain>,
176 ) -> bool {
177 if let Some(checkpoint) = self.lifetimes.peek(key) {
178 match (checkpoint.account_position, account_position) {
179 (Some(previous), Some(incoming)) if incoming <= previous => return false,
180 (Some(_), None) if creation || checkpoint.deleted => return false,
183 _ => {}
184 }
185 if checkpoint.deleted && !creation {
186 return false;
187 }
188 }
189
190 if account_position.is_none() && source_domain.is_some() && source_seq.is_some() {
191 return self.accepts_source_ordering(key, source_seq, source_domain, !creation);
192 }
193 true
194 }
195
196 fn latest_source_seq<'a>(
197 &'a self,
198 key: &str,
199 source_domain: Option<SlotIndexDomain>,
200 ) -> Option<&'a str> {
201 let checkpoint = self.lifetimes.peek(key);
202 if checkpoint.is_some_and(|checkpoint| checkpoint.account_position.is_some()) {
206 return checkpoint.and_then(|checkpoint| checkpoint.source_seq(source_domain));
207 }
208 let checkpoint = checkpoint.and_then(|checkpoint| checkpoint.source_seq(source_domain));
209 if source_domain != Some(SlotIndexDomain::Legacy) {
214 return checkpoint;
215 }
216 let entity = self
217 .entities
218 .peek(key)
219 .and_then(|entity| entity.field("_seq"))
220 .and_then(Value::as_str);
221 match (checkpoint, entity) {
222 (Some(left), Some(right)) if cmp_seq(left, right).is_lt() => Some(right),
223 (Some(left), _) => Some(left),
224 (None, entity) => entity,
225 }
226 }
227
228 fn accepts_source_ordering(
229 &self,
230 key: &str,
231 incoming: Option<&str>,
232 source_domain: Option<SlotIndexDomain>,
233 allow_equal: bool,
234 ) -> bool {
235 match (self.latest_source_seq(key, source_domain), incoming) {
236 (Some(previous), Some(incoming)) => {
237 let ordering = cmp_seq(incoming, previous);
238 ordering.is_gt() || (allow_equal && ordering.is_eq())
239 }
240 (Some(_), None) => false,
243 (None, _) => true,
244 }
245 }
246
247 fn admit(
252 &mut self,
253 key: &str,
254 origin: PatchOrigin,
255 ordering: LifetimeOrdering<'_>,
256 ) -> Admission {
257 let LifetimeOrdering {
258 account_position,
259 source_seq,
260 source_domain,
261 } = ordering;
262 if origin != PatchOrigin::Unknown && !self.creations_marked {
263 self.mark_creations();
264 }
265
266 let creation = origin == PatchOrigin::Creation;
267 if !self.accepts(key, creation, account_position, source_seq, source_domain) {
268 return Admission::Refuse;
269 }
270
271 if let Some(position) = account_position {
272 let previous = self.lifetimes.peek(key).cloned();
279 self.lifetimes.put(
280 key.to_string(),
281 EntityLifetimeCheckpoint {
282 account_position: Some(position),
283 source_seq: previous
284 .as_ref()
285 .and_then(|checkpoint| checkpoint.source_seq.clone()),
286 source_sequences: previous
287 .map(|checkpoint| checkpoint.source_sequences)
288 .unwrap_or_default(),
289 deleted: false,
290 },
291 );
292 self.trim_lifetimes();
293 } else if let (Some(source_seq), Some(source_domain)) = (source_seq, source_domain) {
294 let mut checkpoint = if creation {
295 EntityLifetimeCheckpoint {
296 account_position: None,
297 source_seq: None,
298 source_sequences: BTreeMap::new(),
299 deleted: false,
300 }
301 } else {
302 self.lifetimes
303 .peek(key)
304 .cloned()
305 .unwrap_or(EntityLifetimeCheckpoint {
306 account_position: None,
307 source_seq: None,
308 source_sequences: BTreeMap::new(),
309 deleted: false,
310 })
311 };
312 checkpoint
313 .source_sequences
314 .insert(source_domain, source_seq.to_string());
315 checkpoint.deleted = false;
316 self.lifetimes.put(key.to_string(), checkpoint);
317 self.trim_lifetimes();
318 } else if creation
319 && self.lifetimes.peek(key).is_some_and(|checkpoint| {
320 checkpoint.source_seq.is_none() && checkpoint.source_sequences.is_empty()
321 })
322 {
323 self.lifetimes.pop(key);
326 }
327
328 let whole = match origin {
329 PatchOrigin::Creation => true,
330 PatchOrigin::Change => self.entities.contains(key),
331 PatchOrigin::Unknown => self.entities.contains(key) || !self.was_evicted(key),
332 };
333 if !whole {
334 return Admission::Refuse;
335 }
336 if creation || !self.entities.contains(key) {
339 Admission::Create
340 } else {
341 Admission::Merge
342 }
343 }
344
345 fn accepts_whole(
349 &self,
350 key: &str,
351 requested_account_position: Option<AccountPosition>,
352 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
353 ) -> bool {
354 let Some(checkpoint) = self.lifetimes.peek(key) else {
355 return true;
356 };
357 if checkpoint.deleted || checkpoint.account_position != requested_account_position {
358 return false;
359 }
360 match requested_source {
361 Some((requested_source_seq, requested_source_domain)) => {
362 !((checkpoint.account_position.is_none()
363 && checkpoint.source_seq(Some(requested_source_domain))
364 != requested_source_seq)
365 || self.entities.contains(key))
366 }
367 None => !(checkpoint.account_position.is_some() && self.entities.contains(key)),
368 }
369 }
370
371 fn trim_lifetimes(&mut self) {
372 while self.lifetimes.len()
373 > self
374 .max_entities
375 .saturating_mul(EVICTED_KEYS_PER_CACHED_ENTITY)
376 {
377 self.lifetimes.pop_lru();
378 }
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385pub struct EntityLifetimeCheckpoint {
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub account_position: Option<AccountPosition>,
388 #[serde(skip_serializing_if = "Option::is_none")]
391 pub source_seq: Option<String>,
392 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
395 pub source_sequences: BTreeMap<SlotIndexDomain, String>,
396 pub deleted: bool,
397}
398
399impl EntityLifetimeCheckpoint {
400 fn source_seq(&self, source_domain: Option<SlotIndexDomain>) -> Option<&str> {
401 match source_domain {
402 Some(SlotIndexDomain::Legacy) | None => self
403 .source_sequences
404 .get(&SlotIndexDomain::Legacy)
405 .map(String::as_str)
406 .or(self.source_seq.as_deref()),
407 Some(domain) => self.source_sequences.get(&domain).map(String::as_str),
408 }
409 }
410}
411
412pub type EntityLifetimes = Vec<(String, String, EntityLifetimeCheckpoint)>;
414
415#[derive(Debug, Clone, Copy, Default)]
417pub struct LifetimeOrdering<'a> {
418 pub account_position: Option<AccountPosition>,
419 pub source_seq: Option<&'a str>,
420 pub source_domain: Option<SlotIndexDomain>,
423}
424
425pub type EntityTombstones = Vec<(String, String, Option<String>)>;
428
429pub fn cmp_seq(a: &str, b: &str) -> std::cmp::Ordering {
433 fn parse(s: &str) -> (u64, u64) {
434 let mut parts = s.splitn(2, ':');
435 let slot = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
436 let offset = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
437 (slot, offset)
438 }
439 parse(a).cmp(&parse(b))
440}
441
442fn account_position_from_seq(seq: &str) -> Option<AccountPosition> {
443 let mut parts = seq.splitn(2, ':');
444 Some(AccountPosition::new(
445 parts.next()?.parse().ok()?,
446 parts.next()?.parse().ok()?,
447 ))
448}
449
450#[derive(Debug, Clone)]
452pub struct EntityCacheConfig {
453 pub max_entities_per_view: usize,
455 pub max_array_length: usize,
457 pub initial_snapshot_batch_size: usize,
459 pub subsequent_snapshot_batch_size: usize,
461}
462
463impl Default for EntityCacheConfig {
464 fn default() -> Self {
465 Self {
466 max_entities_per_view: DEFAULT_MAX_ENTITIES_PER_VIEW,
467 max_array_length: DEFAULT_MAX_ARRAY_LENGTH,
468 initial_snapshot_batch_size: DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE,
469 subsequent_snapshot_batch_size: DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE,
470 }
471 }
472}
473
474#[derive(Clone)]
503pub struct EntityCache {
504 caches: Arc<RwLock<HashMap<String, ViewEntries>>>,
506 config: EntityCacheConfig,
507}
508
509impl EntityCache {
510 pub fn new() -> Self {
512 Self::with_config(EntityCacheConfig::default())
513 }
514
515 pub fn with_config(config: EntityCacheConfig) -> Self {
517 Self {
518 caches: Arc::new(RwLock::new(HashMap::new())),
519 config,
520 }
521 }
522
523 pub fn max_entities_per_view(&self) -> usize {
528 self.config.max_entities_per_view
529 }
530
531 pub async fn upsert(&self, view_id: &str, key: &str, patch: Value) -> CacheWrite {
534 self.upsert_with_append(view_id, key, patch, &[], PatchOrigin::Unknown)
535 .await
536 }
537
538 pub async fn upsert_with_append(
542 &self,
543 view_id: &str,
544 key: &str,
545 patch: Value,
546 append_paths: &[String],
547 origin: PatchOrigin,
548 ) -> CacheWrite {
549 self.upsert_with_lifetime(view_id, key, patch, append_paths, origin, None)
550 .await
551 }
552
553 pub async fn upsert_with_lifetime(
557 &self,
558 view_id: &str,
559 key: &str,
560 patch: Value,
561 append_paths: &[String],
562 origin: PatchOrigin,
563 account_position: Option<AccountPosition>,
564 ) -> CacheWrite {
565 let source_seq = patch.get("_seq").and_then(Value::as_str).map(str::to_owned);
566 self.upsert_with_ordering(
567 view_id,
568 key,
569 patch,
570 append_paths,
571 origin,
572 LifetimeOrdering {
573 account_position,
574 source_seq: source_seq.as_deref(),
575 source_domain: (origin == PatchOrigin::Creation).then_some(SlotIndexDomain::Legacy),
579 },
580 )
581 .await
582 }
583
584 pub async fn upsert_with_ordering(
588 &self,
589 view_id: &str,
590 key: &str,
591 patch: Value,
592 append_paths: &[String],
593 origin: PatchOrigin,
594 ordering: LifetimeOrdering<'_>,
595 ) -> CacheWrite {
596 let (patch, version) = split_version(patch);
597 self.upsert_views(
598 key,
599 patch,
600 &[ViewVersion { view_id, version }],
601 append_paths,
602 origin,
603 ordering,
604 )
605 .await
606 .pop()
607 .expect("upsert_views returns one write per view")
608 }
609
610 pub(crate) async fn upsert_views(
621 &self,
622 key: &str,
623 patch: Value,
624 views: &[ViewVersion<'_>],
625 append_paths: &[String],
626 origin: PatchOrigin,
627 ordering: LifetimeOrdering<'_>,
628 ) -> Vec<CacheWrite> {
629 let (mut patch, stray_version) = split_version(patch);
632 let version_of = |index: usize| {
633 views[index]
634 .version
635 .clone()
636 .or_else(|| stray_version.clone())
637 };
638 let max_array_length = self.config.max_array_length;
639 let mut caches = self.caches.write().await;
640
641 let mut admissions: Vec<Admission> = views
642 .iter()
643 .map(|view| {
644 caches
645 .entry(view.view_id.to_string())
646 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view))
647 .admit(key, origin, ordering)
648 })
649 .collect();
650
651 let mut writes = vec![CacheWrite::Merged; views.len()];
652 for (index, admission) in admissions.iter().enumerate() {
653 if *admission == Admission::Refuse {
654 writes[index] = CacheWrite::Refused {
655 patch: with_version(patch.clone(), version_of(index)),
656 };
657 }
658 }
659
660 let placeholder = Arc::new(Value::Null);
663 let mut groups: Vec<(Arc<Value>, Vec<usize>)> = Vec::new();
664 for (index, admission) in admissions.iter_mut().enumerate() {
665 if *admission != Admission::Merge {
666 continue;
667 }
668 let held = caches
669 .get_mut(views[index].view_id)
670 .and_then(|view| view.entities.get_mut(key));
671 let Some(entity) = held else {
672 *admission = Admission::Create;
675 continue;
676 };
677 let fields = std::mem::replace(entity.fields_mut(), placeholder.clone());
678 match groups
679 .iter_mut()
680 .find(|(shared, _)| Arc::ptr_eq(shared, &fields))
681 {
682 Some((_, members)) => members.push(index),
683 None => groups.push((fields, vec![index])),
684 }
685 }
686
687 if admissions.contains(&Admission::Create) {
688 let source = if groups.is_empty() {
689 std::mem::take(&mut patch)
690 } else {
691 patch.clone()
692 };
693 let created = Arc::new(truncate_arrays_if_needed(source, max_array_length));
694 for (index, admission) in admissions.iter().enumerate() {
695 if *admission != Admission::Create {
696 continue;
697 }
698 let version = version_of(index)
699 .filter(|_| created.is_object())
700 .map(|version| truncate_arrays_if_needed(version, max_array_length));
701 if let Some(view) = caches.get_mut(views[index].view_id) {
702 view.store(
703 key.to_string(),
704 SharedEntity::from_parts(created.clone(), version),
705 );
706 }
707 writes[index] = CacheWrite::Created;
708 }
709 }
710
711 let last_group = groups.len().saturating_sub(1);
712 for (group, (mut fields, members)) in groups.into_iter().enumerate() {
713 let mut group_patch = if group == last_group {
714 std::mem::take(&mut patch)
715 } else {
716 patch.clone()
717 };
718 let merge_once = fields.is_object() && group_patch.is_object();
719 if merge_once {
720 deep_merge_with_append(
721 Arc::make_mut(&mut fields),
722 std::mem::take(&mut group_patch),
723 append_paths,
724 max_array_length,
725 );
726 }
727 for index in members {
728 let Some(entity) = caches
729 .get_mut(views[index].view_id)
730 .and_then(|view| view.entities.peek_mut(key))
731 else {
732 continue;
733 };
734 *entity.fields_mut() = fields.clone();
735 if merge_once {
736 let version = entity.version_mut();
737 *version = merge_version(
738 version.take(),
739 version_of(index),
740 append_paths,
741 max_array_length,
742 );
743 } else {
744 merge_shared(
747 entity,
748 with_version(group_patch.clone(), version_of(index)),
749 append_paths,
750 max_array_length,
751 );
752 }
753 }
754 }
755 writes
756 }
757
758 pub async fn store_whole(&self, view_id: &str, key: &str, entity: Value) -> bool {
762 self.store_whole_for_lifetime(view_id, key, entity, None)
763 .await
764 }
765
766 pub async fn store_whole_for_lifetime(
769 &self,
770 view_id: &str,
771 key: &str,
772 entity: Value,
773 requested_account_position: Option<AccountPosition>,
774 ) -> bool {
775 self.store_whole_checked(view_id, key, entity, requested_account_position, None)
776 .await
777 }
778
779 pub async fn store_whole_for_ordering(
780 &self,
781 view_id: &str,
782 key: &str,
783 entity: Value,
784 requested_account_position: Option<AccountPosition>,
785 requested_source_seq: Option<&str>,
786 ) -> bool {
787 self.store_whole_for_ordering_in_domain(
788 view_id,
789 key,
790 entity,
791 requested_account_position,
792 requested_source_seq,
793 SlotIndexDomain::Legacy,
794 )
795 .await
796 }
797
798 pub async fn store_whole_for_ordering_in_domain(
799 &self,
800 view_id: &str,
801 key: &str,
802 entity: Value,
803 requested_account_position: Option<AccountPosition>,
804 requested_source_seq: Option<&str>,
805 requested_source_domain: SlotIndexDomain,
806 ) -> bool {
807 self.store_whole_checked(
808 view_id,
809 key,
810 entity,
811 requested_account_position,
812 Some((requested_source_seq, requested_source_domain)),
813 )
814 .await
815 }
816
817 async fn store_whole_checked(
818 &self,
819 view_id: &str,
820 key: &str,
821 entity: Value,
822 requested_account_position: Option<AccountPosition>,
823 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
824 ) -> bool {
825 let (entity, version) = split_version(entity);
826 self.store_whole_views(
827 key,
828 entity,
829 &[ViewVersion { view_id, version }],
830 requested_account_position,
831 requested_source,
832 )
833 .await
834 .pop()
835 .expect("store_whole_views answers for every view")
836 }
837
838 pub(crate) async fn store_whole_views(
844 &self,
845 key: &str,
846 entity: Value,
847 views: &[ViewVersion<'_>],
848 requested_account_position: Option<AccountPosition>,
849 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
850 ) -> Vec<bool> {
851 let max_array_length = self.config.max_array_length;
852 let (entity, stray_version) = split_version(entity);
853 let fields = Arc::new(truncate_arrays_if_needed(entity, max_array_length));
854 let mut caches = self.caches.write().await;
855 views
856 .iter()
857 .map(|view| {
858 let entries = caches
859 .entry(view.view_id.to_string())
860 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
861 if !entries.accepts_whole(key, requested_account_position, requested_source) {
862 return false;
863 }
864 let version = view
865 .version
866 .clone()
867 .or_else(|| stray_version.clone())
868 .filter(|_| fields.is_object())
869 .map(|version| truncate_arrays_if_needed(version, max_array_length));
870 entries.store(
871 key.to_string(),
872 SharedEntity::from_parts(fields.clone(), version),
873 );
874 true
875 })
876 .collect()
877 }
878
879 pub async fn account_position(&self, view_id: &str, key: &str) -> Option<AccountPosition> {
880 self.caches
881 .read()
882 .await
883 .get(view_id)
884 .and_then(|view| view.lifetimes.peek(key))
885 .and_then(|checkpoint| checkpoint.account_position)
886 }
887
888 pub async fn accepts_mutation(
889 &self,
890 view_id: &str,
891 key: &str,
892 _entity: &Value,
893 creation: bool,
894 ) -> bool {
895 self.accepts_lifetime_mutation(view_id, key, creation, None)
896 .await
897 }
898
899 pub async fn accepts_lifetime_mutation(
900 &self,
901 view_id: &str,
902 key: &str,
903 creation: bool,
904 account_position: Option<AccountPosition>,
905 ) -> bool {
906 self.accepts_ordered_lifetime_mutation(view_id, key, creation, account_position, None)
907 .await
908 }
909
910 pub async fn accepts_ordered_lifetime_mutation(
911 &self,
912 view_id: &str,
913 key: &str,
914 creation: bool,
915 account_position: Option<AccountPosition>,
916 source_seq: Option<&str>,
917 ) -> bool {
918 self.accepts_ordered_lifetime_mutation_in_domain(
919 view_id,
920 key,
921 creation,
922 account_position,
923 source_seq,
924 SlotIndexDomain::Legacy,
925 )
926 .await
927 }
928
929 pub async fn accepts_ordered_lifetime_mutation_in_domain(
930 &self,
931 view_id: &str,
932 key: &str,
933 creation: bool,
934 account_position: Option<AccountPosition>,
935 source_seq: Option<&str>,
936 source_domain: SlotIndexDomain,
937 ) -> bool {
938 self.caches.read().await.get(view_id).is_none_or(|view| {
939 view.accepts(
940 key,
941 creation,
942 account_position,
943 source_seq,
944 Some(source_domain),
945 )
946 })
947 }
948
949 pub async fn delete(
953 &self,
954 view_id: &str,
955 key: &str,
956 account_position: Option<AccountPosition>,
957 ) -> bool {
958 match account_position {
959 Some(position) => {
960 self.delete_ordered(view_id, key, Some(position), None)
961 .await
962 }
963 None => self.delete_current(view_id, key).await,
964 }
965 }
966
967 pub async fn delete_current(&self, view_id: &str, key: &str) -> bool {
971 let mut caches = self.caches.write().await;
972 let view = caches
973 .entry(view_id.to_string())
974 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
975 let checkpoint = view
976 .lifetimes
977 .peek(key)
978 .cloned()
979 .unwrap_or(EntityLifetimeCheckpoint {
980 account_position: None,
981 source_seq: None,
982 source_sequences: BTreeMap::new(),
983 deleted: false,
984 });
985 if checkpoint.deleted {
986 return true;
987 }
988 view.entities.pop(key);
989 view.forget_evicted(key);
990 view.lifetimes.put(
991 key.to_string(),
992 EntityLifetimeCheckpoint {
993 deleted: true,
994 ..checkpoint
995 },
996 );
997 view.trim_lifetimes();
998 true
999 }
1000
1001 pub async fn delete_ordered(
1004 &self,
1005 view_id: &str,
1006 key: &str,
1007 account_position: Option<AccountPosition>,
1008 source_seq: Option<&str>,
1009 ) -> bool {
1010 self.delete_ordered_in_domain(
1011 view_id,
1012 key,
1013 account_position,
1014 source_seq,
1015 SlotIndexDomain::Legacy,
1016 )
1017 .await
1018 }
1019
1020 pub async fn delete_ordered_in_domain(
1021 &self,
1022 view_id: &str,
1023 key: &str,
1024 account_position: Option<AccountPosition>,
1025 source_seq: Option<&str>,
1026 source_domain: SlotIndexDomain,
1027 ) -> bool {
1028 let mut caches = self.caches.write().await;
1029 let view = caches
1030 .entry(view_id.to_string())
1031 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1032 if let Some(checkpoint) = view.lifetimes.peek(key) {
1033 match (checkpoint.account_position, account_position) {
1034 (Some(previous), Some(incoming)) if incoming < previous => return false,
1035 (Some(previous), Some(incoming)) if incoming == previous => {
1036 return checkpoint.deleted;
1037 }
1038 _ => {}
1039 }
1040 }
1041 if account_position.is_none()
1042 && source_seq.is_some()
1043 && !view.accepts_source_ordering(key, source_seq, Some(source_domain), true)
1044 {
1045 return false;
1046 }
1047 let previous = view.lifetimes.peek(key).cloned();
1048 view.entities.pop(key);
1049 view.forget_evicted(key);
1050 let mut checkpoint = if account_position.is_some() {
1051 let previous = previous.unwrap_or(EntityLifetimeCheckpoint {
1052 account_position: None,
1053 source_seq: None,
1054 source_sequences: BTreeMap::new(),
1055 deleted: false,
1056 });
1057 EntityLifetimeCheckpoint {
1058 account_position,
1059 source_seq: previous.source_seq,
1060 source_sequences: previous.source_sequences,
1061 deleted: true,
1062 }
1063 } else {
1064 previous.unwrap_or(EntityLifetimeCheckpoint {
1065 account_position: None,
1066 source_seq: None,
1067 source_sequences: BTreeMap::new(),
1068 deleted: false,
1069 })
1070 };
1071 if account_position.is_none() {
1072 if let Some(source_seq) = source_seq {
1073 checkpoint
1074 .source_sequences
1075 .insert(source_domain, source_seq.to_string());
1076 }
1077 checkpoint.deleted = true;
1078 }
1079 view.lifetimes.put(key.to_string(), checkpoint);
1080 view.trim_lifetimes();
1081 true
1082 }
1083
1084 pub async fn dump_lifetimes(&self) -> EntityLifetimes {
1085 self.caches
1086 .read()
1087 .await
1088 .iter()
1089 .flat_map(|(id, view)| {
1090 view.lifetimes
1091 .iter()
1092 .map(|(key, checkpoint)| (id.clone(), key.clone(), checkpoint.clone()))
1093 })
1094 .collect()
1095 }
1096
1097 pub async fn hydrate_lifetimes(&self, lifetimes: EntityLifetimes) {
1098 let mut caches = self.caches.write().await;
1099 for (id, key, checkpoint) in lifetimes.into_iter().rev() {
1100 let view = caches
1101 .entry(id)
1102 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1103 view.lifetimes.put(key, checkpoint);
1104 view.trim_lifetimes();
1105 }
1106 }
1107
1108 pub async fn hydrate_legacy_tombstones(&self, tombstones: EntityTombstones) {
1112 let mut caches = self.caches.write().await;
1113 for (id, key, seq) in tombstones.into_iter().rev() {
1114 let view = caches
1115 .entry(id)
1116 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1117 let deleted = !view.entities.contains(&key);
1118 let account_position = seq.as_deref().and_then(account_position_from_seq);
1119 view.lifetimes.put(
1120 key,
1121 EntityLifetimeCheckpoint {
1122 account_position,
1123 source_seq: None,
1124 source_sequences: BTreeMap::new(),
1125 deleted,
1126 },
1127 );
1128 view.trim_lifetimes();
1129 }
1130 }
1131
1132 pub async fn deletion_is_current(&self, view_id: &str, key: &str) -> bool {
1136 let caches = self.caches.read().await;
1137 caches.get(view_id).is_none_or(|view| {
1138 view.lifetimes
1139 .peek(key)
1140 .map_or(!view.entities.contains(key), |checkpoint| {
1141 checkpoint.deleted
1142 })
1143 })
1144 }
1145
1146 pub fn merge_patch(&self, base: &mut Value, patch: Value, append_paths: &[String]) {
1149 deep_merge_with_append(base, patch, append_paths, self.config.max_array_length);
1150 }
1151
1152 pub fn merge_patch_shared(
1155 &self,
1156 entity: &mut SharedEntity,
1157 patch: Value,
1158 append_paths: &[String],
1159 ) {
1160 merge_shared(entity, patch, append_paths, self.config.max_array_length);
1161 }
1162
1163 pub async fn remember_evicted(&self, view_id: &str, keys: impl IntoIterator<Item = String>) {
1169 let mut caches = self.caches.write().await;
1170 let view = caches
1171 .entry(view_id.to_string())
1172 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1173 for key in keys {
1174 if !view.entities.contains(&key) {
1175 view.remember_evicted(key);
1176 }
1177 }
1178 }
1179
1180 pub async fn get_all(&self, view_id: &str) -> Vec<(String, Value)> {
1186 self.get_all_shared(view_id)
1187 .await
1188 .into_iter()
1189 .map(|(key, entity)| (key, entity.into_value()))
1190 .collect()
1191 }
1192
1193 pub async fn get_all_shared(&self, view_id: &str) -> Vec<(String, SharedEntity)> {
1196 let caches = self.caches.read().await;
1197
1198 caches
1199 .get(view_id)
1200 .map(|view| {
1201 view.entities
1202 .iter()
1203 .map(|(k, v)| (k.clone(), v.clone()))
1204 .collect()
1205 })
1206 .unwrap_or_default()
1207 }
1208
1209 pub async fn get_after(
1215 &self,
1216 view_id: &str,
1217 cursor: &str,
1218 limit: Option<usize>,
1219 ) -> Vec<(String, Value)> {
1220 let caches = self.caches.read().await;
1221
1222 if let Some(view) = caches.get(view_id) {
1223 let mut results: Vec<(String, &SharedEntity)> = view
1224 .entities
1225 .iter()
1226 .filter(|(_, entity)| {
1227 entity
1228 .field("_seq")
1229 .and_then(|s| s.as_str())
1230 .map(|seq| cmp_seq(seq, cursor) == std::cmp::Ordering::Greater)
1231 .unwrap_or(false)
1232 })
1233 .map(|(k, v)| (k.clone(), v))
1234 .collect();
1235
1236 results.sort_by(|a, b| {
1238 let seq_a = a.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1239 let seq_b = b.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1240 cmp_seq(seq_a, seq_b)
1241 });
1242
1243 if let Some(limit) = limit {
1245 results.truncate(limit);
1246 }
1247
1248 results
1249 .into_iter()
1250 .map(|(key, entity)| (key, entity.to_value()))
1251 .collect()
1252 } else {
1253 vec![]
1254 }
1255 }
1256
1257 pub async fn get(&self, view_id: &str, key: &str) -> Option<Value> {
1259 self.get_shared(view_id, key)
1260 .await
1261 .map(SharedEntity::into_value)
1262 }
1263
1264 pub async fn get_shared(&self, view_id: &str, key: &str) -> Option<SharedEntity> {
1267 let caches = self.caches.read().await;
1268 caches
1269 .get(view_id)
1270 .and_then(|view| view.entities.peek(key).cloned())
1271 }
1272
1273 pub async fn remove(&self, view_id: &str, key: &str) -> Option<Value> {
1278 let removed = {
1279 let mut caches = self.caches.write().await;
1280 caches.get_mut(view_id).and_then(|view| {
1281 view.forget_evicted(key);
1282 view.entities.pop(key)
1283 })
1284 };
1285 removed.map(SharedEntity::into_value)
1286 }
1287
1288 pub async fn len(&self, view_id: &str) -> usize {
1290 let caches = self.caches.read().await;
1291 caches
1292 .get(view_id)
1293 .map(|view| view.entities.len())
1294 .unwrap_or(0)
1295 }
1296
1297 pub async fn is_empty(&self, view_id: &str) -> bool {
1299 self.len(view_id).await == 0
1300 }
1301
1302 pub fn snapshot_config(&self) -> SnapshotBatchConfig {
1304 SnapshotBatchConfig {
1305 initial_batch_size: self.config.initial_snapshot_batch_size,
1306 subsequent_batch_size: self.config.subsequent_snapshot_batch_size,
1307 }
1308 }
1309
1310 pub async fn clear(&self, view_id: &str) {
1312 let mut caches = self.caches.write().await;
1313 if let Some(view) = caches.get_mut(view_id) {
1314 view.entities.clear();
1315 view.evicted = None;
1316 }
1317 }
1318
1319 pub async fn clear_all(&self) {
1320 let mut caches = self.caches.write().await;
1321 caches.clear();
1322 }
1323
1324 pub async fn dump(&self) -> Vec<(String, Vec<(String, Value)>)> {
1330 self.dump_shared()
1331 .await
1332 .into_iter()
1333 .map(|(view_id, entries)| {
1334 (
1335 view_id,
1336 entries
1337 .into_iter()
1338 .map(|(key, entity)| (key, entity.into_value()))
1339 .collect(),
1340 )
1341 })
1342 .collect()
1343 }
1344
1345 pub async fn dump_shared(&self) -> Vec<(String, Vec<(String, SharedEntity)>)> {
1348 let caches = self.caches.read().await;
1349 caches
1350 .iter()
1351 .map(|(view_id, view)| {
1352 (
1353 view_id.clone(),
1354 view.entities
1355 .iter()
1356 .map(|(key, entity)| (key.clone(), entity.clone()))
1357 .collect(),
1358 )
1359 })
1360 .collect()
1361 }
1362
1363 pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
1371 let mut caches = self.caches.write().await;
1372 caches.clear();
1373 let mut restored: HashMap<(String, String), Vec<Arc<Value>>> = HashMap::new();
1374 for (view_id, entries) in views {
1375 let export = view_id.split('/').next().unwrap_or_default().to_string();
1376 let view = caches
1377 .entry(view_id)
1378 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1379 for (key, entity) in entries.into_iter().rev() {
1380 let (fields, version) = SharedEntity::new(entity).into_parts();
1381 let seen = restored.entry((export.clone(), key.clone())).or_default();
1382 let fields = match seen.iter().find(|held| **held == fields) {
1383 Some(held) => held.clone(),
1384 None => {
1385 seen.push(fields.clone());
1386 fields
1387 }
1388 };
1389 view.store(key, SharedEntity::from_parts(fields, version));
1390 }
1391 }
1392 }
1393
1394 pub async fn stats(&self) -> CacheStats {
1395 let caches = self.caches.read().await;
1396 let mut total_entities = 0;
1397 let mut views = Vec::new();
1398
1399 for (view_id, view) in caches.iter() {
1400 let count = view.entities.len();
1401 total_entities += count;
1402 views.push((view_id.clone(), count));
1403 }
1404
1405 views.sort_by_key(|b| std::cmp::Reverse(b.1));
1406
1407 CacheStats {
1408 view_count: caches.len(),
1409 total_entities,
1410 top_views: views.into_iter().take(5).collect(),
1411 }
1412 }
1413}
1414
1415#[derive(Debug, Clone, PartialEq, Eq)]
1417pub struct CacheStats {
1418 pub view_count: usize,
1419 pub total_entities: usize,
1420 pub top_views: Vec<(String, usize)>,
1421}
1422
1423#[derive(Debug, Clone, Copy)]
1424pub struct SnapshotBatchConfig {
1425 pub initial_batch_size: usize,
1426 pub subsequent_batch_size: usize,
1427}
1428
1429impl Default for EntityCache {
1430 fn default() -> Self {
1431 Self::new()
1432 }
1433}
1434
1435fn deep_merge_with_append(
1436 base: &mut Value,
1437 patch: Value,
1438 append_paths: &[String],
1439 max_array_length: usize,
1440) {
1441 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
1442}
1443
1444fn deep_merge_with_append_inner(
1445 base: &mut Value,
1446 patch: Value,
1447 append_paths: &[String],
1448 current_path: &str,
1449 max_array_length: usize,
1450) {
1451 match (base, patch) {
1452 (Value::Object(base_map), Value::Object(patch_map)) => {
1453 for (key, patch_value) in patch_map {
1454 let child_path = if current_path.is_empty() {
1455 key.clone()
1456 } else {
1457 format!("{}.{}", current_path, key)
1458 };
1459
1460 if let Some(base_value) = base_map.get_mut(&key) {
1461 deep_merge_with_append_inner(
1462 base_value,
1463 patch_value,
1464 append_paths,
1465 &child_path,
1466 max_array_length,
1467 );
1468 } else {
1469 base_map.insert(
1470 key,
1471 truncate_arrays_if_needed(patch_value, max_array_length),
1472 );
1473 }
1474 }
1475 }
1476
1477 (Value::Array(base_arr), Value::Array(patch_arr)) => {
1478 let should_append = append_paths.iter().any(|p| p == current_path);
1479 if should_append {
1480 base_arr.extend(patch_arr);
1481 if base_arr.len() > max_array_length {
1482 let excess = base_arr.len() - max_array_length;
1483 base_arr.drain(0..excess);
1484 }
1485 } else {
1486 *base_arr = patch_arr;
1487 if base_arr.len() > max_array_length {
1488 let excess = base_arr.len() - max_array_length;
1489 base_arr.drain(0..excess);
1490 }
1491 }
1492 }
1493
1494 (base, patch_value) => {
1495 *base = truncate_arrays_if_needed(patch_value, max_array_length);
1496 }
1497 }
1498}
1499
1500fn truncate_arrays_if_needed(mut value: Value, max_array_length: usize) -> Value {
1503 truncate_arrays_in_place(&mut value, max_array_length);
1504 value
1505}
1506
1507fn truncate_arrays_in_place(value: &mut Value, max_array_length: usize) {
1510 match value {
1511 Value::Array(arr) => {
1512 if arr.len() > max_array_length {
1513 let excess = arr.len() - max_array_length;
1514 arr.drain(0..excess);
1515 }
1516 for element in arr {
1517 truncate_arrays_in_place(element, max_array_length);
1518 }
1519 }
1520 Value::Object(map) => {
1521 for field in map.values_mut() {
1522 truncate_arrays_in_place(field, max_array_length);
1523 }
1524 }
1525 _ => {}
1526 }
1527}
1528
1529fn merge_shared(
1533 entity: &mut SharedEntity,
1534 patch: Value,
1535 append_paths: &[String],
1536 max_array_length: usize,
1537) {
1538 let (patch, patch_version) = split_version(patch);
1539 if entity.fields().is_object() && patch.is_object() {
1540 deep_merge_with_append(
1541 Arc::make_mut(entity.fields_mut()),
1542 patch,
1543 append_paths,
1544 max_array_length,
1545 );
1546 let version = entity.version_mut();
1547 *version = merge_version(
1548 version.take(),
1549 patch_version,
1550 append_paths,
1551 max_array_length,
1552 );
1553 return;
1554 }
1555 let (fields, _) = std::mem::replace(entity, SharedEntity::new(Value::Null)).into_parts();
1559 let mut whole = match (&*fields, &patch) {
1560 (Value::Array(_), Value::Array(_)) => {
1561 Arc::try_unwrap(fields).unwrap_or_else(|shared| Value::clone(&shared))
1562 }
1563 _ => Value::Null,
1564 };
1565 deep_merge_with_append(
1566 &mut whole,
1567 with_version(patch, patch_version),
1568 append_paths,
1569 max_array_length,
1570 );
1571 *entity = SharedEntity::new(whole);
1572}
1573
1574fn merge_version(
1577 current: Option<Value>,
1578 incoming: Option<Value>,
1579 append_paths: &[String],
1580 max_array_length: usize,
1581) -> Option<Value> {
1582 match (current, incoming) {
1583 (Some(mut current), Some(incoming)) => {
1584 deep_merge_with_append_inner(
1585 &mut current,
1586 incoming,
1587 append_paths,
1588 VERSION_FIELD,
1589 max_array_length,
1590 );
1591 Some(current)
1592 }
1593 (None, Some(incoming)) => Some(truncate_arrays_if_needed(incoming, max_array_length)),
1594 (current, None) => current,
1595 }
1596}
1597
1598#[cfg(test)]
1599mod tests {
1600 use super::*;
1601 use serde_json::json;
1602
1603 #[tokio::test]
1604 async fn same_slot_instruction_activity_cannot_block_authoritative_deletion() {
1605 let cache = EntityCache::new();
1606 assert_eq!(
1607 cache
1608 .upsert_with_lifetime(
1609 "v",
1610 "a",
1611 json!({"account":true,"_seq":"100:000000000009"}),
1612 &[],
1613 PatchOrigin::Creation,
1614 Some(AccountPosition::new(100, 9)),
1615 )
1616 .await,
1617 CacheWrite::Created
1618 );
1619 assert_eq!(
1620 cache
1621 .upsert_with_append(
1622 "v",
1623 "a",
1624 json!({"instruction":true,"_seq":"100:000000000900"}),
1625 &[],
1626 PatchOrigin::Change
1627 )
1628 .await,
1629 CacheWrite::Merged
1630 );
1631 assert!(
1632 cache
1633 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1634 .await
1635 );
1636 assert!(cache.get("v", "a").await.is_none());
1637 assert!(cache.deletion_is_current("v", "a").await);
1638 }
1639
1640 #[tokio::test]
1641 async fn recreation_has_its_own_ordering_and_replaces_old_fields() {
1642 let cache = EntityCache::with_config(EntityCacheConfig {
1643 max_entities_per_view: 1,
1644 ..Default::default()
1645 });
1646 assert!(
1647 cache
1648 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1649 .await
1650 );
1651 assert_eq!(
1652 cache
1653 .upsert_with_lifetime(
1654 "v",
1655 "a",
1656 json!({"fresh":true, "_seq":"100:000000001002"}),
1657 &[],
1658 PatchOrigin::Creation,
1659 Some(AccountPosition::new(100, 1002)),
1660 )
1661 .await,
1662 CacheWrite::Created
1663 );
1664 assert_eq!(
1665 cache
1666 .upsert_with_append(
1667 "v",
1668 "a",
1669 json!({"instruction":true, "_seq":"100:000000000900"}),
1670 &[],
1671 PatchOrigin::Change
1672 )
1673 .await,
1674 CacheWrite::Merged
1675 );
1676
1677 assert!(matches!(
1680 cache
1681 .upsert_with_lifetime(
1682 "v",
1683 "a",
1684 json!({"old":true, "_seq":"100:000000001001"}),
1685 &[],
1686 PatchOrigin::Change,
1687 Some(AccountPosition::new(100, 1001)),
1688 )
1689 .await,
1690 CacheWrite::Refused { .. }
1691 ));
1692 assert!(
1693 !cache
1694 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1695 .await
1696 );
1697 assert!(
1698 !cache
1699 .store_whole("v", "a", json!({"old":true, "_seq":"100:000000000900"}))
1700 .await
1701 );
1702 let row = cache.get("v", "a").await.unwrap();
1703 assert_eq!(row["fresh"], true);
1704 assert_eq!(row["instruction"], true);
1705 assert!(row.get("old").is_none());
1706
1707 cache
1708 .upsert_with_append(
1709 "v",
1710 "other",
1711 json!({"other":true}),
1712 &[],
1713 PatchOrigin::Creation,
1714 )
1715 .await;
1716 assert!(cache.get("v", "a").await.is_none());
1717 assert!(matches!(
1718 cache
1719 .upsert_with_lifetime(
1720 "v",
1721 "a",
1722 json!({"accountUpdate":true, "_seq":"100:000000001003"}),
1723 &[],
1724 PatchOrigin::Change,
1725 Some(AccountPosition::new(100, 1003)),
1726 )
1727 .await,
1728 CacheWrite::Refused { .. }
1729 ));
1730 assert!(
1731 !cache
1732 .store_whole_for_lifetime(
1733 "v",
1734 "a",
1735 row.clone(),
1736 Some(AccountPosition::new(100, 1002)),
1737 )
1738 .await
1739 );
1740 let mut current = row;
1741 current["accountUpdate"] = Value::Bool(true);
1742 current["_seq"] = Value::String("100:000000001003".into());
1743 assert!(
1744 cache
1745 .store_whole_for_lifetime("v", "a", current, Some(AccountPosition::new(100, 1003)),)
1746 .await
1747 );
1748
1749 let restored = EntityCache::new();
1752 restored.hydrate(cache.dump().await).await;
1753 restored
1754 .hydrate_lifetimes(cache.dump_lifetimes().await)
1755 .await;
1756 assert_eq!(
1757 restored
1758 .upsert_with_append(
1759 "v",
1760 "a",
1761 json!({"afterRestore":true, "_seq":"100:000000000901"}),
1762 &[],
1763 PatchOrigin::Change,
1764 )
1765 .await,
1766 CacheWrite::Merged
1767 );
1768 assert!(matches!(
1769 restored
1770 .upsert_with_lifetime(
1771 "v",
1772 "a",
1773 json!({"old":true, "_seq":"100:000000001001"}),
1774 &[],
1775 PatchOrigin::Change,
1776 Some(AccountPosition::new(100, 1001)),
1777 )
1778 .await,
1779 CacheWrite::Refused { .. }
1780 ));
1781 }
1782
1783 #[tokio::test]
1784 async fn recreation_retains_each_source_cursor_as_an_old_lifetime_barrier() {
1785 let cache = EntityCache::new();
1786 assert_eq!(
1787 cache
1788 .upsert_with_lifetime(
1789 "v",
1790 "a",
1791 json!({"lifetime":"old"}),
1792 &[],
1793 PatchOrigin::Creation,
1794 Some(AccountPosition::new(100, 9)),
1795 )
1796 .await,
1797 CacheWrite::Created
1798 );
1799 for (domain, seq, field) in [
1800 (
1801 SlotIndexDomain::Instruction,
1802 "100:000000000800",
1803 "oldInstruction",
1804 ),
1805 (SlotIndexDomain::Resolver, "100:900000000000", "oldResolver"),
1806 ] {
1807 let mut patch = json!({"_seq":seq});
1808 patch[field] = Value::Bool(true);
1809 assert_eq!(
1810 cache
1811 .upsert_with_ordering(
1812 "v",
1813 "a",
1814 patch,
1815 &[],
1816 PatchOrigin::Change,
1817 LifetimeOrdering {
1818 account_position: None,
1819 source_seq: Some(seq),
1820 source_domain: Some(domain),
1821 },
1822 )
1823 .await,
1824 CacheWrite::Merged
1825 );
1826 }
1827
1828 assert!(
1829 cache
1830 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1831 .await
1832 );
1833 assert_eq!(
1834 cache
1835 .upsert_with_lifetime(
1836 "v",
1837 "a",
1838 json!({"lifetime":"new"}),
1839 &[],
1840 PatchOrigin::Creation,
1841 Some(AccountPosition::new(100, 11)),
1842 )
1843 .await,
1844 CacheWrite::Created
1845 );
1846
1847 for (domain, seq, field) in [
1850 (
1851 SlotIndexDomain::Instruction,
1852 "100:000000000799",
1853 "staleInstruction",
1854 ),
1855 (
1856 SlotIndexDomain::Resolver,
1857 "100:899999999999",
1858 "staleResolver",
1859 ),
1860 ] {
1861 let mut patch = json!({"_seq":seq});
1862 patch[field] = Value::Bool(true);
1863 assert!(matches!(
1864 cache
1865 .upsert_with_ordering(
1866 "v",
1867 "a",
1868 patch,
1869 &[],
1870 PatchOrigin::Change,
1871 LifetimeOrdering {
1872 account_position: None,
1873 source_seq: Some(seq),
1874 source_domain: Some(domain),
1875 },
1876 )
1877 .await,
1878 CacheWrite::Refused { .. }
1879 ));
1880 }
1881
1882 assert_eq!(
1885 cache
1886 .upsert_with_ordering(
1887 "v",
1888 "a",
1889 json!({"newInstruction":true, "_seq":"100:000000000900"}),
1890 &[],
1891 PatchOrigin::Change,
1892 LifetimeOrdering {
1893 account_position: None,
1894 source_seq: Some("100:000000000900"),
1895 source_domain: Some(SlotIndexDomain::Instruction),
1896 },
1897 )
1898 .await,
1899 CacheWrite::Merged
1900 );
1901 let row = cache.get("v", "a").await.unwrap();
1902 assert_eq!(row["lifetime"], "new");
1903 assert_eq!(row["newInstruction"], true);
1904 assert!(row.get("oldInstruction").is_none());
1905 assert!(row.get("oldResolver").is_none());
1906 assert!(row.get("staleInstruction").is_none());
1907 assert!(row.get("staleResolver").is_none());
1908
1909 let restored = EntityCache::new();
1910 restored.hydrate(cache.dump().await).await;
1911 restored
1912 .hydrate_lifetimes(cache.dump_lifetimes().await)
1913 .await;
1914 assert!(matches!(
1915 restored
1916 .upsert_with_ordering(
1917 "v",
1918 "a",
1919 json!({"staleAfterRestore":true}),
1920 &[],
1921 PatchOrigin::Change,
1922 LifetimeOrdering {
1923 account_position: None,
1924 source_seq: Some("100:000000000799"),
1925 source_domain: Some(SlotIndexDomain::Instruction),
1926 },
1927 )
1928 .await,
1929 CacheWrite::Refused { .. }
1930 ));
1931 }
1932
1933 #[tokio::test]
1934 async fn markerless_sources_keep_seq_ordering_across_recreation_and_restore() {
1935 let cache = EntityCache::new();
1936 assert_eq!(
1937 cache
1938 .upsert_with_append(
1939 "v",
1940 "a",
1941 json!({"old":true,"_seq":"100:000000000010"}),
1942 &[],
1943 PatchOrigin::Creation,
1944 )
1945 .await,
1946 CacheWrite::Created
1947 );
1948 assert!(
1949 !cache
1950 .delete_ordered("v", "a", None, Some("100:000000000009"))
1951 .await
1952 );
1953 assert!(cache.get("v", "a").await.is_some());
1954 assert!(
1955 cache
1956 .delete_ordered("v", "a", None, Some("100:000000000011"))
1957 .await
1958 );
1959 assert_eq!(
1960 cache
1961 .upsert_with_append(
1962 "v",
1963 "a",
1964 json!({"fresh":true,"_seq":"100:000000000012"}),
1965 &[],
1966 PatchOrigin::Creation,
1967 )
1968 .await,
1969 CacheWrite::Created
1970 );
1971 assert!(matches!(
1972 cache
1973 .upsert_with_append(
1974 "v",
1975 "a",
1976 json!({"stale":true,"_seq":"100:000000000010"}),
1977 &[],
1978 PatchOrigin::Creation,
1979 )
1980 .await,
1981 CacheWrite::Refused { .. }
1982 ));
1983 assert!(
1984 !cache
1985 .delete_ordered("v", "a", None, Some("100:000000000011"))
1986 .await
1987 );
1988
1989 let restored = EntityCache::new();
1990 restored.hydrate(cache.dump().await).await;
1991 restored
1992 .hydrate_lifetimes(cache.dump_lifetimes().await)
1993 .await;
1994 assert!(
1995 !restored
1996 .delete_ordered("v", "a", None, Some("100:000000000011"))
1997 .await
1998 );
1999 let row = restored.get("v", "a").await.unwrap();
2000 assert_eq!(row["fresh"], true);
2001 assert!(row.get("old").is_none());
2002 assert!(row.get("stale").is_none());
2003
2004 let resync_cache = EntityCache::new();
2007 resync_cache
2008 .hydrate_lifetimes(cache.dump_lifetimes().await)
2009 .await;
2010 assert!(
2011 !resync_cache
2012 .store_whole_for_ordering(
2013 "v",
2014 "a",
2015 json!({"stale_resend":true}),
2016 None,
2017 Some("100:000000000010"),
2018 )
2019 .await
2020 );
2021 assert!(
2022 resync_cache
2023 .store_whole_for_ordering(
2024 "v",
2025 "a",
2026 json!({"fresh_resend":true}),
2027 None,
2028 Some("100:000000000012"),
2029 )
2030 .await
2031 );
2032 }
2033
2034 #[tokio::test]
2035 async fn older_source_patch_cannot_replace_newer_row_or_move_its_cursor_back() {
2036 let cache = EntityCache::new();
2037 assert_eq!(
2038 cache
2039 .upsert_with_append(
2040 "v",
2041 "a",
2042 json!({"value":"created","_seq":"100:000000000010"}),
2043 &[],
2044 PatchOrigin::Creation,
2045 )
2046 .await,
2047 CacheWrite::Created
2048 );
2049 assert_eq!(
2050 cache
2051 .upsert_with_ordering(
2052 "v",
2053 "a",
2054 json!({"value":"newer","_seq":"100:000000000012"}),
2055 &[],
2056 PatchOrigin::Change,
2057 LifetimeOrdering {
2058 account_position: None,
2059 source_seq: Some("100:000000000012"),
2060 source_domain: Some(SlotIndexDomain::Legacy),
2061 },
2062 )
2063 .await,
2064 CacheWrite::Merged
2065 );
2066 assert!(matches!(
2067 cache
2068 .upsert_with_ordering(
2069 "v",
2070 "a",
2071 json!({"value":"older","stale":true,"_seq":"100:000000000011"}),
2072 &[],
2073 PatchOrigin::Change,
2074 LifetimeOrdering {
2075 account_position: None,
2076 source_seq: Some("100:000000000011"),
2077 source_domain: Some(SlotIndexDomain::Legacy),
2078 },
2079 )
2080 .await,
2081 CacheWrite::Refused { .. }
2082 ));
2083 assert!(
2084 !cache
2085 .delete_ordered("v", "a", None, Some("100:000000000011"))
2086 .await
2087 );
2088 let row = cache.get("v", "a").await.unwrap();
2089 assert_eq!(row["value"], "newer");
2090 assert_eq!(row["_seq"], "100:000000000012");
2091 assert!(row.get("stale").is_none());
2092 }
2093
2094 #[tokio::test]
2095 async fn independent_source_recency_domains_survive_snapshot_restore() {
2096 let cache = EntityCache::new();
2097 cache
2098 .upsert_with_lifetime(
2099 "v",
2100 "a",
2101 json!({"value":"created"}),
2102 &[],
2103 PatchOrigin::Creation,
2104 Some(AccountPosition::new(100, 9)),
2105 )
2106 .await;
2107 for (domain, seq, value) in [
2108 (SlotIndexDomain::Resolver, "100:900000000000", "resolver"),
2109 (
2110 SlotIndexDomain::Instruction,
2111 "100:000000000900",
2112 "instruction",
2113 ),
2114 ] {
2115 assert_eq!(
2116 cache
2117 .upsert_with_ordering(
2118 "v",
2119 "a",
2120 json!({"value":value, "_seq":seq}),
2121 &[],
2122 PatchOrigin::Change,
2123 LifetimeOrdering {
2124 account_position: None,
2125 source_seq: Some(seq),
2126 source_domain: Some(domain),
2127 },
2128 )
2129 .await,
2130 CacheWrite::Merged
2131 );
2132 }
2133
2134 let restored = EntityCache::new();
2135 restored.hydrate(cache.dump().await).await;
2136 restored
2137 .hydrate_lifetimes(cache.dump_lifetimes().await)
2138 .await;
2139
2140 for (domain, seq) in [
2141 (SlotIndexDomain::Instruction, "100:000000000899"),
2142 (SlotIndexDomain::Resolver, "100:899999999999"),
2143 ] {
2144 assert!(matches!(
2145 restored
2146 .upsert_with_ordering(
2147 "v",
2148 "a",
2149 json!({"stale":true, "_seq":seq}),
2150 &[],
2151 PatchOrigin::Change,
2152 LifetimeOrdering {
2153 account_position: None,
2154 source_seq: Some(seq),
2155 source_domain: Some(domain),
2156 },
2157 )
2158 .await,
2159 CacheWrite::Refused { .. }
2160 ));
2161 }
2162 assert_eq!(
2163 restored.get("v", "a").await.unwrap()["value"],
2164 "instruction"
2165 );
2166 }
2167
2168 #[tokio::test]
2169 async fn explicit_current_delete_accepts_a_source_without_position() {
2170 let cache = EntityCache::new();
2171 cache
2172 .upsert_with_append(
2173 "v",
2174 "a",
2175 json!({"value":true,"_seq":"100:000000000010"}),
2176 &[],
2177 PatchOrigin::Creation,
2178 )
2179 .await;
2180
2181 assert!(cache.delete("v", "a", None).await);
2182 assert!(cache.get("v", "a").await.is_none());
2183 assert!(cache.deletion_is_current("v", "a").await);
2184 }
2185
2186 #[tokio::test]
2187 async fn evicted_live_row_does_not_make_an_earlier_delete_current() {
2188 let cache = EntityCache::with_config(EntityCacheConfig {
2189 max_entities_per_view: 1,
2190 ..Default::default()
2191 });
2192 cache
2193 .upsert_with_lifetime(
2194 "v",
2195 "a",
2196 json!({"value":true,"_seq":"100:000000000010"}),
2197 &[],
2198 PatchOrigin::Creation,
2199 Some(AccountPosition::new(100, 10)),
2200 )
2201 .await;
2202 cache
2203 .upsert_with_append("v", "b", json!({"value":true}), &[], PatchOrigin::Creation)
2204 .await;
2205
2206 assert!(cache.get("v", "a").await.is_none());
2207 assert!(!cache.deletion_is_current("v", "a").await);
2208 assert!(cache.delete_current("v", "a").await);
2209 assert!(cache.deletion_is_current("v", "a").await);
2210 }
2211
2212 #[tokio::test]
2213 async fn test_basic_upsert_and_get() {
2214 let cache = EntityCache::new();
2215
2216 cache
2217 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
2218 .await;
2219
2220 let entity = cache.get("tokens/list", "abc123").await;
2221 assert!(entity.is_some());
2222 assert_eq!(entity.unwrap()["name"], "Test Token");
2223 }
2224
2225 #[tokio::test]
2226 async fn test_deep_merge_objects() {
2227 let cache = EntityCache::new();
2228
2229 cache
2230 .upsert(
2231 "tokens/list",
2232 "abc123",
2233 json!({
2234 "id": "abc123",
2235 "metrics": {"volume": 100}
2236 }),
2237 )
2238 .await;
2239
2240 cache
2241 .upsert(
2242 "tokens/list",
2243 "abc123",
2244 json!({
2245 "metrics": {"trades": 50}
2246 }),
2247 )
2248 .await;
2249
2250 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2251 assert_eq!(entity["id"], "abc123");
2252 assert_eq!(entity["metrics"]["volume"], 100);
2253 assert_eq!(entity["metrics"]["trades"], 50);
2254 }
2255
2256 #[tokio::test]
2257 async fn test_array_append() {
2258 let cache = EntityCache::new();
2259
2260 cache
2261 .upsert(
2262 "tokens/list",
2263 "abc123",
2264 json!({
2265 "events": [{"type": "buy", "amount": 100}]
2266 }),
2267 )
2268 .await;
2269
2270 cache
2271 .upsert_with_append(
2272 "tokens/list",
2273 "abc123",
2274 json!({
2275 "events": [{"type": "sell", "amount": 50}]
2276 }),
2277 &["events".to_string()],
2278 PatchOrigin::Unknown,
2279 )
2280 .await;
2281
2282 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2283 let events = entity["events"].as_array().unwrap();
2284 assert_eq!(events.len(), 2);
2285 assert_eq!(events[0]["type"], "buy");
2286 assert_eq!(events[1]["type"], "sell");
2287 }
2288
2289 #[tokio::test]
2290 async fn test_array_lru_eviction() {
2291 let config = EntityCacheConfig {
2292 max_entities_per_view: 1000,
2293 max_array_length: 3,
2294 ..Default::default()
2295 };
2296 let cache = EntityCache::with_config(config);
2297
2298 cache
2299 .upsert(
2300 "tokens/list",
2301 "abc123",
2302 json!({
2303 "events": [
2304 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
2305 ]
2306 }),
2307 )
2308 .await;
2309
2310 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2311 let events = entity["events"].as_array().unwrap();
2312
2313 assert_eq!(events.len(), 3);
2314 assert_eq!(events[0]["id"], 3);
2315 assert_eq!(events[1]["id"], 4);
2316 assert_eq!(events[2]["id"], 5);
2317 }
2318
2319 #[tokio::test]
2320 async fn test_array_append_with_lru() {
2321 let config = EntityCacheConfig {
2322 max_entities_per_view: 1000,
2323 max_array_length: 3,
2324 ..Default::default()
2325 };
2326 let cache = EntityCache::with_config(config);
2327
2328 cache
2329 .upsert(
2330 "tokens/list",
2331 "abc123",
2332 json!({
2333 "events": [{"id": 1}, {"id": 2}]
2334 }),
2335 )
2336 .await;
2337
2338 cache
2339 .upsert_with_append(
2340 "tokens/list",
2341 "abc123",
2342 json!({
2343 "events": [{"id": 3}, {"id": 4}]
2344 }),
2345 &["events".to_string()],
2346 PatchOrigin::Unknown,
2347 )
2348 .await;
2349
2350 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2351 let events = entity["events"].as_array().unwrap();
2352
2353 assert_eq!(events.len(), 3);
2355 assert_eq!(events[0]["id"], 2);
2356 assert_eq!(events[1]["id"], 3);
2357 assert_eq!(events[2]["id"], 4);
2358 }
2359
2360 #[tokio::test]
2361 async fn test_entity_lru_eviction() {
2362 let config = EntityCacheConfig {
2363 max_entities_per_view: 2,
2364 max_array_length: 100,
2365 ..Default::default()
2366 };
2367 let cache = EntityCache::with_config(config);
2368
2369 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2370 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2371 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
2372
2373 assert!(cache.get("tokens/list", "key1").await.is_none());
2374 assert!(cache.get("tokens/list", "key2").await.is_some());
2375 assert!(cache.get("tokens/list", "key3").await.is_some());
2376 }
2377
2378 #[tokio::test]
2379 async fn a_patch_for_an_evicted_key_is_refused_not_stored() {
2380 let cache = EntityCache::with_config(EntityCacheConfig {
2381 max_entities_per_view: 2,
2382 ..Default::default()
2383 });
2384 for id in 1..=3 {
2385 assert_eq!(
2386 cache
2387 .upsert(
2388 "tokens/list",
2389 &format!("key{id}"),
2390 json!({"id": id, "n": 0})
2391 )
2392 .await,
2393 CacheWrite::Created
2394 );
2395 }
2396 assert_eq!(
2398 cache.upsert("tokens/list", "key1", json!({"n": 5})).await,
2399 CacheWrite::Refused {
2400 patch: json!({"n": 5})
2401 }
2402 );
2403 assert!(cache.get("tokens/list", "key1").await.is_none());
2404 assert_eq!(
2405 cache.upsert("tokens/list", "key3", json!({"n": 1})).await,
2406 CacheWrite::Merged
2407 );
2408
2409 cache.remove("tokens/list", "key1").await;
2411 assert_eq!(
2412 cache
2413 .upsert("tokens/list", "key1", json!({"id": 1, "n": 6}))
2414 .await,
2415 CacheWrite::Created
2416 );
2417 }
2418
2419 #[tokio::test]
2420 async fn a_whole_entity_ends_an_eviction() {
2421 let cache = EntityCache::with_config(EntityCacheConfig {
2422 max_entities_per_view: 1,
2423 ..Default::default()
2424 });
2425 cache.upsert("v", "a", json!({"id": "a", "n": 0})).await;
2426 cache.upsert("v", "b", json!({"id": "b", "n": 0})).await;
2427 assert!(matches!(
2428 cache.upsert("v", "a", json!({"n": 1})).await,
2429 CacheWrite::Refused { .. }
2430 ));
2431 cache
2432 .store_whole("v", "a", json!({"id": "a", "n": 1}))
2433 .await;
2434 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2435 assert_eq!(cache.len("v").await, 1, "still bounded: b made room");
2436 assert_eq!(
2437 cache.upsert("v", "a", json!({"n": 2})).await,
2438 CacheWrite::Merged
2439 );
2440 }
2441
2442 #[tokio::test]
2443 async fn the_evicted_key_memory_is_bounded() {
2444 let cache = EntityCache::with_config(EntityCacheConfig {
2445 max_entities_per_view: 1,
2446 ..Default::default()
2447 });
2448 let remembered = EVICTED_KEYS_PER_CACHED_ENTITY;
2449 for key in 0..=remembered + 1 {
2450 cache
2451 .upsert("v", &key.to_string(), json!({"id": key}))
2452 .await;
2453 }
2454 assert_eq!(
2457 cache.upsert("v", "0", json!({"id": 0})).await,
2458 CacheWrite::Created
2459 );
2460 assert!(matches!(
2461 cache
2462 .upsert("v", &remembered.to_string(), json!({"n": 1}))
2463 .await,
2464 CacheWrite::Refused { .. }
2465 ));
2466 }
2467
2468 async fn write(
2469 cache: &EntityCache,
2470 key: &str,
2471 patch: Value,
2472 origin: PatchOrigin,
2473 ) -> CacheWrite {
2474 cache.upsert_with_append("v", key, patch, &[], origin).await
2475 }
2476
2477 #[tokio::test]
2480 async fn an_unmarked_patch_for_a_key_not_held_is_refused_however_old() {
2481 let cache = EntityCache::with_config(EntityCacheConfig {
2482 max_entities_per_view: 1,
2483 ..Default::default()
2484 });
2485 let evictions = EVICTED_KEYS_PER_CACHED_ENTITY * 4;
2486 for key in 0..=evictions {
2487 assert_eq!(
2488 write(
2489 &cache,
2490 &key.to_string(),
2491 json!({"id": key}),
2492 PatchOrigin::Creation
2493 )
2494 .await,
2495 CacheWrite::Created,
2496 "a creation is stored as soon as it arrives"
2497 );
2498 }
2499 assert_eq!(
2501 write(&cache, "0", json!({"n": 1}), PatchOrigin::Change).await,
2502 CacheWrite::Refused {
2503 patch: json!({"n": 1})
2504 }
2505 );
2506 assert!(
2507 matches!(
2508 write(&cache, "never", json!({"n": 1}), PatchOrigin::Change).await,
2509 CacheWrite::Refused { .. }
2510 ),
2511 "nor does it matter whether the view ever held the key"
2512 );
2513 assert_eq!(cache.get("v", "0").await, None);
2514 assert_eq!(
2515 write(
2516 &cache,
2517 &evictions.to_string(),
2518 json!({"n": 1}),
2519 PatchOrigin::Change
2520 )
2521 .await,
2522 CacheWrite::Merged
2523 );
2524 assert_eq!(
2526 write(&cache, "0", json!({"id": 0, "n": 2}), PatchOrigin::Creation).await,
2527 CacheWrite::Created
2528 );
2529 assert_eq!(cache.get("v", "0").await, Some(json!({"id": 0, "n": 2})));
2530 }
2531
2532 #[tokio::test]
2535 async fn a_creation_for_a_key_held_replaces() {
2536 let cache = EntityCache::new();
2537 write(
2538 &cache,
2539 "a",
2540 json!({"id": "a", "old": 1}),
2541 PatchOrigin::Creation,
2542 )
2543 .await;
2544 assert_eq!(
2545 write(
2546 &cache,
2547 "a",
2548 json!({"id": "a", "n": 1}),
2549 PatchOrigin::Creation
2550 )
2551 .await,
2552 CacheWrite::Created
2553 );
2554 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2555 }
2556
2557 async fn remembers_evictions(cache: &EntityCache, view_id: &str) -> bool {
2558 cache
2559 .caches
2560 .read()
2561 .await
2562 .get(view_id)
2563 .is_some_and(|view| view.evicted.is_some())
2564 }
2565
2566 #[tokio::test]
2570 async fn a_view_fed_marked_creations_keeps_no_eviction_memory() {
2571 let cache = EntityCache::with_config(EntityCacheConfig {
2572 max_entities_per_view: 1,
2573 ..Default::default()
2574 });
2575 cache.remember_evicted("v", ["restored".to_string()]).await;
2576 assert!(remembers_evictions(&cache, "v").await);
2577
2578 write(&cache, "a", json!({"id": "a"}), PatchOrigin::Creation).await;
2579 assert!(!remembers_evictions(&cache, "v").await);
2580 write(&cache, "b", json!({"id": "b"}), PatchOrigin::Creation).await;
2581 assert!(!remembers_evictions(&cache, "v").await, "a was evicted");
2582 assert!(matches!(
2583 write(&cache, "restored", json!({"n": 1}), PatchOrigin::Change).await,
2584 CacheWrite::Refused { .. }
2585 ));
2586 assert!(matches!(
2587 write(&cache, "a", json!({"n": 1}), PatchOrigin::Change).await,
2588 CacheWrite::Refused { .. }
2589 ));
2590 }
2591
2592 #[tokio::test]
2593 async fn remembered_keys_are_refused_unless_held() {
2594 let cache = EntityCache::new();
2595 cache.upsert("tokens/list", "held", json!({"id": 1})).await;
2596 cache
2597 .remember_evicted("tokens/list", ["held".to_string(), "gone".to_string()])
2598 .await;
2599 assert_eq!(
2600 cache.upsert("tokens/list", "held", json!({"n": 1})).await,
2601 CacheWrite::Merged
2602 );
2603 assert!(matches!(
2604 cache.upsert("tokens/list", "gone", json!({"n": 1})).await,
2605 CacheWrite::Refused { .. }
2606 ));
2607 assert_eq!(
2609 cache.upsert("tokens/state", "gone", json!({"id": 2})).await,
2610 CacheWrite::Created
2611 );
2612 }
2613
2614 #[tokio::test]
2615 async fn test_get_all() {
2616 let cache = EntityCache::new();
2617
2618 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2619 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2620
2621 let all = cache.get_all("tokens/list").await;
2622 assert_eq!(all.len(), 2);
2623 }
2624
2625 #[tokio::test]
2626 async fn remove_is_scoped_to_one_entity() {
2627 let cache = EntityCache::new();
2628 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
2629 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
2630
2631 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
2632 assert!(cache.get("tokens/list", "one").await.is_none());
2633 assert!(cache.get("tokens/list", "two").await.is_some());
2634 }
2635
2636 #[tokio::test]
2637 async fn test_separate_views() {
2638 let cache = EntityCache::new();
2639
2640 cache
2641 .upsert("tokens/list", "key1", json!({"type": "token"}))
2642 .await;
2643 cache
2644 .upsert("games/list", "key1", json!({"type": "game"}))
2645 .await;
2646
2647 let token = cache.get("tokens/list", "key1").await.unwrap();
2648 let game = cache.get("games/list", "key1").await.unwrap();
2649
2650 assert_eq!(token["type"], "token");
2651 assert_eq!(game["type"], "game");
2652 }
2653
2654 #[test]
2655 fn test_deep_merge_with_append() {
2656 let mut base = json!({
2657 "a": 1,
2658 "b": {"c": 2},
2659 "arr": [1, 2]
2660 });
2661
2662 let patch = json!({
2663 "b": {"d": 3},
2664 "arr": [3],
2665 "e": 4
2666 });
2667
2668 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
2669
2670 assert_eq!(base["a"], 1);
2671 assert_eq!(base["b"]["c"], 2);
2672 assert_eq!(base["b"]["d"], 3);
2673 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
2674 assert_eq!(base["e"], 4);
2675 }
2676
2677 #[test]
2678 fn test_deep_merge_replace_array() {
2679 let mut base = json!({
2680 "arr": [1, 2, 3]
2681 });
2682
2683 let patch = json!({
2684 "arr": [4, 5]
2685 });
2686
2687 deep_merge_with_append(&mut base, patch, &[], 100);
2688
2689 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
2690 assert_eq!(base["arr"][0], 4);
2691 assert_eq!(base["arr"][1], 5);
2692 }
2693
2694 #[test]
2695 fn test_deep_merge_nested_append() {
2696 let mut base = json!({
2697 "stats": {"events": [1, 2]}
2698 });
2699
2700 let patch = json!({
2701 "stats": {"events": [3]}
2702 });
2703
2704 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
2705
2706 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
2707 }
2708
2709 #[test]
2710 fn test_snapshot_config_defaults() {
2711 let cache = EntityCache::new();
2712 let config = cache.snapshot_config();
2713
2714 assert_eq!(config.initial_batch_size, 50);
2715 assert_eq!(config.subsequent_batch_size, 100);
2716 }
2717
2718 #[test]
2719 fn test_snapshot_config_custom() {
2720 let config = EntityCacheConfig {
2721 initial_snapshot_batch_size: 25,
2722 subsequent_snapshot_batch_size: 75,
2723 ..Default::default()
2724 };
2725 let cache = EntityCache::with_config(config);
2726 let snapshot_config = cache.snapshot_config();
2727
2728 assert_eq!(snapshot_config.initial_batch_size, 25);
2729 assert_eq!(snapshot_config.subsequent_batch_size, 75);
2730 }
2731
2732 #[tokio::test]
2733 async fn test_get_after() {
2734 let cache = EntityCache::new();
2735
2736 cache
2738 .upsert(
2739 "tokens/list",
2740 "key1",
2741 json!({"id": 1, "_seq": "100:000000000001"}),
2742 )
2743 .await;
2744 cache
2745 .upsert(
2746 "tokens/list",
2747 "key2",
2748 json!({"id": 2, "_seq": "100:000000000002"}),
2749 )
2750 .await;
2751 cache
2752 .upsert(
2753 "tokens/list",
2754 "key3",
2755 json!({"id": 3, "_seq": "100:000000000003"}),
2756 )
2757 .await;
2758 cache
2759 .upsert(
2760 "tokens/list",
2761 "key4",
2762 json!({"id": 4, "_seq": "101:000000000001"}),
2763 )
2764 .await;
2765
2766 let after = cache
2768 .get_after("tokens/list", "100:000000000002", None)
2769 .await;
2770
2771 assert_eq!(after.len(), 2);
2773 assert_eq!(after[0].0, "key3");
2774 assert_eq!(after[1].0, "key4");
2775 }
2776
2777 #[tokio::test]
2778 async fn test_get_after_with_limit() {
2779 let cache = EntityCache::new();
2780
2781 cache
2783 .upsert(
2784 "tokens/list",
2785 "key1",
2786 json!({"id": 1, "_seq": "100:000000000001"}),
2787 )
2788 .await;
2789 cache
2790 .upsert(
2791 "tokens/list",
2792 "key2",
2793 json!({"id": 2, "_seq": "100:000000000002"}),
2794 )
2795 .await;
2796 cache
2797 .upsert(
2798 "tokens/list",
2799 "key3",
2800 json!({"id": 3, "_seq": "100:000000000003"}),
2801 )
2802 .await;
2803
2804 let after = cache
2806 .get_after("tokens/list", "100:000000000000", Some(2))
2807 .await;
2808
2809 assert_eq!(after.len(), 2);
2811 assert_eq!(after[0].0, "key1");
2812 assert_eq!(after[1].0, "key2");
2813 }
2814
2815 #[tokio::test]
2816 async fn test_get_after_empty_result() {
2817 let cache = EntityCache::new();
2818
2819 cache
2820 .upsert(
2821 "tokens/list",
2822 "key1",
2823 json!({"id": 1, "_seq": "100:000000000001"}),
2824 )
2825 .await;
2826
2827 let after = cache
2829 .get_after("tokens/list", "999:000000000000", None)
2830 .await;
2831
2832 assert!(after.is_empty());
2833 }
2834
2835 #[tokio::test]
2836 async fn test_get_after_missing_seq() {
2837 let cache = EntityCache::new();
2838
2839 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2841
2842 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
2844
2845 assert!(after.is_empty());
2846 }
2847
2848 fn versions<'a>(views: &[(&'a str, &str)]) -> Vec<ViewVersion<'a>> {
2849 views
2850 .iter()
2851 .map(|(view_id, version)| ViewVersion {
2852 view_id,
2853 version: Some(json!(version)),
2854 })
2855 .collect()
2856 }
2857
2858 const VIEWS: [&str; 3] = ["t/list", "t/state", "t/append"];
2859
2860 async fn shared(cache: &EntityCache, key: &str) -> Vec<SharedEntity> {
2861 let mut entities = Vec::new();
2862 for view in VIEWS {
2863 entities.push(cache.get_shared(view, key).await.unwrap());
2864 }
2865 entities
2866 }
2867
2868 async fn write_all(
2869 cache: &EntityCache,
2870 key: &str,
2871 patch: Value,
2872 stamps: [&str; 3],
2873 origin: PatchOrigin,
2874 ) -> Vec<CacheWrite> {
2875 let views: Vec<_> = VIEWS.into_iter().zip(stamps).collect();
2876 cache
2877 .upsert_views(
2878 key,
2879 patch,
2880 &versions(&views),
2881 &["events".to_string()],
2882 origin,
2883 LifetimeOrdering::default(),
2884 )
2885 .await
2886 }
2887
2888 #[tokio::test]
2892 async fn views_written_together_share_one_copy() {
2893 let cache = EntityCache::new();
2894 let writes = write_all(
2895 &cache,
2896 "a",
2897 json!({"id": "a", "events": [1]}),
2898 ["e:1", "e:2", "e:3"],
2899 PatchOrigin::Creation,
2900 )
2901 .await;
2902 assert_eq!(writes, vec![CacheWrite::Created; 3]);
2903 let [list, state, append] =
2904 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
2905 assert!(list.shares_fields_with(&state) && list.shares_fields_with(&append));
2906 assert_eq!(Arc::strong_count(list.fields()), 6);
2908 assert_eq!(
2909 cache.get("t/state", "a").await,
2910 Some(json!({"id": "a", "events": [1], "_version": "e:2"}))
2911 );
2912
2913 let writes = write_all(
2914 &cache,
2915 "a",
2916 json!({"n": 1, "events": [2]}),
2917 ["e:4", "e:5", "e:6"],
2918 PatchOrigin::Unknown,
2919 )
2920 .await;
2921 assert_eq!(writes, vec![CacheWrite::Merged; 3]);
2922 let merged = shared(&cache, "a").await;
2923 assert!(
2924 merged[0].shares_fields_with(&merged[1]) && merged[0].shares_fields_with(&merged[2])
2925 );
2926 for (view, version) in VIEWS.into_iter().zip(["e:4", "e:5", "e:6"]) {
2927 assert_eq!(
2928 cache.get(view, "a").await,
2929 Some(json!({"id": "a", "n": 1, "events": [1, 2], "_version": version})),
2930 "{view}"
2931 );
2932 }
2933 assert_eq!(
2935 list.to_value(),
2936 json!({"id": "a", "events": [1], "_version": "e:1"})
2937 );
2938 assert!(!list.shares_fields_with(&merged[0]));
2939 }
2940
2941 #[tokio::test]
2944 async fn a_merge_nobody_else_holds_happens_in_place() {
2945 let cache = EntityCache::new();
2946 write_all(
2947 &cache,
2948 "a",
2949 json!({"id": "a"}),
2950 ["e:1", "e:2", "e:3"],
2951 PatchOrigin::Creation,
2952 )
2953 .await;
2954 let before = Arc::as_ptr(cache.get_shared("t/list", "a").await.unwrap().fields());
2955 write_all(
2956 &cache,
2957 "a",
2958 json!({"n": 1}),
2959 ["e:4", "e:5", "e:6"],
2960 PatchOrigin::Unknown,
2961 )
2962 .await;
2963 let after = cache.get_shared("t/list", "a").await.unwrap();
2964 assert_eq!(Arc::as_ptr(after.fields()), before);
2967 assert_eq!(Arc::strong_count(after.fields()), 4);
2968 assert_eq!(after["n"], 1);
2969 }
2970
2971 #[tokio::test]
2974 async fn a_patch_to_one_view_does_not_reach_another() {
2975 let cache = EntityCache::new();
2976 write_all(
2977 &cache,
2978 "a",
2979 json!({"id": "a", "nested": {"n": 0}}),
2980 ["e:1", "e:2", "e:3"],
2981 PatchOrigin::Creation,
2982 )
2983 .await;
2984 assert_eq!(
2985 cache
2986 .upsert_with_append(
2987 "t/list",
2988 "a",
2989 json!({"nested": {"n": 1}, "_version": "e:4"}),
2990 &[],
2991 PatchOrigin::Unknown,
2992 )
2993 .await,
2994 CacheWrite::Merged
2995 );
2996 assert_eq!(
2997 cache.get("t/list", "a").await,
2998 Some(json!({"id": "a", "nested": {"n": 1}, "_version": "e:4"}))
2999 );
3000 for (view, version) in [("t/state", "e:2"), ("t/append", "e:3")] {
3001 assert_eq!(
3002 cache.get(view, "a").await,
3003 Some(json!({"id": "a", "nested": {"n": 0}, "_version": version})),
3004 "{view}"
3005 );
3006 }
3007 let [list, state, append] =
3008 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
3009 assert!(!list.shares_fields_with(&state));
3010 assert!(state.shares_fields_with(&append));
3011 }
3012
3013 #[tokio::test]
3016 async fn a_view_that_refuses_a_patch_keeps_its_copy() {
3017 let cache = EntityCache::new();
3018 write_all(
3019 &cache,
3020 "a",
3021 json!({"id": "a", "n": 0}),
3022 ["e:1", "e:2", "e:3"],
3023 PatchOrigin::Creation,
3024 )
3025 .await;
3026 assert!(cache.delete_current("t/state", "a").await);
3028 let writes = write_all(
3029 &cache,
3030 "a",
3031 json!({"n": 1}),
3032 ["e:4", "e:5", "e:6"],
3033 PatchOrigin::Change,
3034 )
3035 .await;
3036 assert_eq!(writes[0], CacheWrite::Merged);
3037 assert_eq!(
3038 writes[1],
3039 CacheWrite::Refused {
3040 patch: json!({"n": 1, "_version": "e:5"})
3041 }
3042 );
3043 assert_eq!(writes[2], CacheWrite::Merged);
3044 assert_eq!(cache.get("t/state", "a").await, None);
3045 let list = cache.get_shared("t/list", "a").await.unwrap();
3046 let append = cache.get_shared("t/append", "a").await.unwrap();
3047 assert!(list.shares_fields_with(&append));
3048 assert_eq!(
3049 append.to_value(),
3050 json!({"id": "a", "n": 1, "_version": "e:6"})
3051 );
3052 }
3053
3054 #[tokio::test]
3057 async fn a_restore_shares_the_copies_it_saved_per_view() {
3058 let cache = EntityCache::new();
3059 write_all(
3060 &cache,
3061 "a",
3062 json!({"id": "a", "events": [1, 2]}),
3063 ["e:1", "e:2", "e:3"],
3064 PatchOrigin::Creation,
3065 )
3066 .await;
3067 let mut dump = cache.dump().await;
3068 dump.push((
3071 "other/list".to_string(),
3072 vec![("a".to_string(), json!({"id": "a", "events": [1, 2]}))],
3073 ));
3074 dump.sort_by(|left, right| left.0.cmp(&right.0));
3075
3076 let restored = EntityCache::new();
3077 restored.hydrate(dump.clone()).await;
3078 let other = restored.get_shared("other/list", "a").await.unwrap();
3079 let copies = shared(&restored, "a").await;
3080 assert!(!other.shares_fields_with(&copies[0]));
3081 assert!(
3082 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3083 );
3084 let mut again = restored.dump().await;
3085 again.sort_by(|left, right| left.0.cmp(&right.0));
3086 assert_eq!(again, dump);
3087 }
3088
3089 #[tokio::test]
3091 async fn a_whole_entity_stored_in_several_views_is_one_copy() {
3092 let cache = EntityCache::new();
3093 let views = versions(&[("t/list", "e:1"), ("t/state", "e:2"), ("t/append", "e:3")]);
3094 let stored = cache
3095 .store_whole_views("a", json!({"id": "a"}), &views, None, None)
3096 .await;
3097 assert_eq!(stored, vec![true; 3]);
3098 let copies = shared(&cache, "a").await;
3099 assert!(
3100 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3101 );
3102 assert_eq!(
3103 cache.get("t/append", "a").await,
3104 Some(json!({"id": "a", "_version": "e:3"}))
3105 );
3106 }
3107
3108 #[test]
3111 fn a_shared_merge_matches_the_whole_value_merge() {
3112 let cases = [
3113 (
3114 json!({"a": 1, "_version": "e:1"}),
3115 json!({"b": 2, "_version": "e:2"}),
3116 ),
3117 (json!({"a": 1, "_version": "e:1"}), json!({"b": 2})),
3118 (json!({"a": 1, "_version": "e:1"}), json!([1, 2])),
3119 (json!([1, 2]), json!([3])),
3120 (json!([1, 2]), json!({"a": 1, "_version": "e:2"})),
3121 (json!("text"), json!(5)),
3122 (
3123 json!({"_version": "e:1", "list": [1]}),
3124 json!({"list": [2, 3, 4]}),
3125 ),
3126 ];
3127 for (base, patch) in cases {
3128 let append = ["list".to_string()];
3129 let mut expected = base.clone();
3130 deep_merge_with_append(&mut expected, patch.clone(), &append, 2);
3131 let mut entity = SharedEntity::new(base.clone());
3132 let other = entity.clone();
3133 merge_shared(&mut entity, patch.clone(), &append, 2);
3134 assert_eq!(entity.to_value(), expected, "{base} <- {patch}");
3135 assert_eq!(other.to_value(), base, "the other holder's copy");
3136 }
3137 }
3138}