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)>)> {
1329 let caches = self.caches.read().await;
1330 caches
1331 .iter()
1332 .map(|(view_id, view)| {
1333 (
1334 view_id.clone(),
1335 view.entities
1336 .iter()
1337 .map(|(key, entity)| (key.clone(), entity.to_value()))
1338 .collect(),
1339 )
1340 })
1341 .collect()
1342 }
1343
1344 pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
1352 let mut caches = self.caches.write().await;
1353 caches.clear();
1354 let mut restored: HashMap<(String, String), Vec<Arc<Value>>> = HashMap::new();
1355 for (view_id, entries) in views {
1356 let export = view_id.split('/').next().unwrap_or_default().to_string();
1357 let view = caches
1358 .entry(view_id)
1359 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1360 for (key, entity) in entries.into_iter().rev() {
1361 let (fields, version) = SharedEntity::new(entity).into_parts();
1362 let seen = restored.entry((export.clone(), key.clone())).or_default();
1363 let fields = match seen.iter().find(|held| **held == fields) {
1364 Some(held) => held.clone(),
1365 None => {
1366 seen.push(fields.clone());
1367 fields
1368 }
1369 };
1370 view.store(key, SharedEntity::from_parts(fields, version));
1371 }
1372 }
1373 }
1374
1375 pub async fn stats(&self) -> CacheStats {
1376 let caches = self.caches.read().await;
1377 let mut total_entities = 0;
1378 let mut views = Vec::new();
1379
1380 for (view_id, view) in caches.iter() {
1381 let count = view.entities.len();
1382 total_entities += count;
1383 views.push((view_id.clone(), count));
1384 }
1385
1386 views.sort_by_key(|b| std::cmp::Reverse(b.1));
1387
1388 CacheStats {
1389 view_count: caches.len(),
1390 total_entities,
1391 top_views: views.into_iter().take(5).collect(),
1392 }
1393 }
1394}
1395
1396#[derive(Debug, Clone, PartialEq, Eq)]
1398pub struct CacheStats {
1399 pub view_count: usize,
1400 pub total_entities: usize,
1401 pub top_views: Vec<(String, usize)>,
1402}
1403
1404#[derive(Debug, Clone, Copy)]
1405pub struct SnapshotBatchConfig {
1406 pub initial_batch_size: usize,
1407 pub subsequent_batch_size: usize,
1408}
1409
1410impl Default for EntityCache {
1411 fn default() -> Self {
1412 Self::new()
1413 }
1414}
1415
1416fn deep_merge_with_append(
1417 base: &mut Value,
1418 patch: Value,
1419 append_paths: &[String],
1420 max_array_length: usize,
1421) {
1422 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
1423}
1424
1425fn deep_merge_with_append_inner(
1426 base: &mut Value,
1427 patch: Value,
1428 append_paths: &[String],
1429 current_path: &str,
1430 max_array_length: usize,
1431) {
1432 match (base, patch) {
1433 (Value::Object(base_map), Value::Object(patch_map)) => {
1434 for (key, patch_value) in patch_map {
1435 let child_path = if current_path.is_empty() {
1436 key.clone()
1437 } else {
1438 format!("{}.{}", current_path, key)
1439 };
1440
1441 if let Some(base_value) = base_map.get_mut(&key) {
1442 deep_merge_with_append_inner(
1443 base_value,
1444 patch_value,
1445 append_paths,
1446 &child_path,
1447 max_array_length,
1448 );
1449 } else {
1450 base_map.insert(
1451 key,
1452 truncate_arrays_if_needed(patch_value, max_array_length),
1453 );
1454 }
1455 }
1456 }
1457
1458 (Value::Array(base_arr), Value::Array(patch_arr)) => {
1459 let should_append = append_paths.iter().any(|p| p == current_path);
1460 if should_append {
1461 base_arr.extend(patch_arr);
1462 if base_arr.len() > max_array_length {
1463 let excess = base_arr.len() - max_array_length;
1464 base_arr.drain(0..excess);
1465 }
1466 } else {
1467 *base_arr = patch_arr;
1468 if base_arr.len() > max_array_length {
1469 let excess = base_arr.len() - max_array_length;
1470 base_arr.drain(0..excess);
1471 }
1472 }
1473 }
1474
1475 (base, patch_value) => {
1476 *base = truncate_arrays_if_needed(patch_value, max_array_length);
1477 }
1478 }
1479}
1480
1481fn truncate_arrays_if_needed(mut value: Value, max_array_length: usize) -> Value {
1484 truncate_arrays_in_place(&mut value, max_array_length);
1485 value
1486}
1487
1488fn truncate_arrays_in_place(value: &mut Value, max_array_length: usize) {
1491 match value {
1492 Value::Array(arr) => {
1493 if arr.len() > max_array_length {
1494 let excess = arr.len() - max_array_length;
1495 arr.drain(0..excess);
1496 }
1497 for element in arr {
1498 truncate_arrays_in_place(element, max_array_length);
1499 }
1500 }
1501 Value::Object(map) => {
1502 for field in map.values_mut() {
1503 truncate_arrays_in_place(field, max_array_length);
1504 }
1505 }
1506 _ => {}
1507 }
1508}
1509
1510fn merge_shared(
1514 entity: &mut SharedEntity,
1515 patch: Value,
1516 append_paths: &[String],
1517 max_array_length: usize,
1518) {
1519 let (patch, patch_version) = split_version(patch);
1520 if entity.fields().is_object() && patch.is_object() {
1521 deep_merge_with_append(
1522 Arc::make_mut(entity.fields_mut()),
1523 patch,
1524 append_paths,
1525 max_array_length,
1526 );
1527 let version = entity.version_mut();
1528 *version = merge_version(
1529 version.take(),
1530 patch_version,
1531 append_paths,
1532 max_array_length,
1533 );
1534 return;
1535 }
1536 let (fields, _) = std::mem::replace(entity, SharedEntity::new(Value::Null)).into_parts();
1540 let mut whole = match (&*fields, &patch) {
1541 (Value::Array(_), Value::Array(_)) => {
1542 Arc::try_unwrap(fields).unwrap_or_else(|shared| Value::clone(&shared))
1543 }
1544 _ => Value::Null,
1545 };
1546 deep_merge_with_append(
1547 &mut whole,
1548 with_version(patch, patch_version),
1549 append_paths,
1550 max_array_length,
1551 );
1552 *entity = SharedEntity::new(whole);
1553}
1554
1555fn merge_version(
1558 current: Option<Value>,
1559 incoming: Option<Value>,
1560 append_paths: &[String],
1561 max_array_length: usize,
1562) -> Option<Value> {
1563 match (current, incoming) {
1564 (Some(mut current), Some(incoming)) => {
1565 deep_merge_with_append_inner(
1566 &mut current,
1567 incoming,
1568 append_paths,
1569 VERSION_FIELD,
1570 max_array_length,
1571 );
1572 Some(current)
1573 }
1574 (None, Some(incoming)) => Some(truncate_arrays_if_needed(incoming, max_array_length)),
1575 (current, None) => current,
1576 }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581 use super::*;
1582 use serde_json::json;
1583
1584 #[tokio::test]
1585 async fn same_slot_instruction_activity_cannot_block_authoritative_deletion() {
1586 let cache = EntityCache::new();
1587 assert_eq!(
1588 cache
1589 .upsert_with_lifetime(
1590 "v",
1591 "a",
1592 json!({"account":true,"_seq":"100:000000000009"}),
1593 &[],
1594 PatchOrigin::Creation,
1595 Some(AccountPosition::new(100, 9)),
1596 )
1597 .await,
1598 CacheWrite::Created
1599 );
1600 assert_eq!(
1601 cache
1602 .upsert_with_append(
1603 "v",
1604 "a",
1605 json!({"instruction":true,"_seq":"100:000000000900"}),
1606 &[],
1607 PatchOrigin::Change
1608 )
1609 .await,
1610 CacheWrite::Merged
1611 );
1612 assert!(
1613 cache
1614 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1615 .await
1616 );
1617 assert!(cache.get("v", "a").await.is_none());
1618 assert!(cache.deletion_is_current("v", "a").await);
1619 }
1620
1621 #[tokio::test]
1622 async fn recreation_has_its_own_ordering_and_replaces_old_fields() {
1623 let cache = EntityCache::with_config(EntityCacheConfig {
1624 max_entities_per_view: 1,
1625 ..Default::default()
1626 });
1627 assert!(
1628 cache
1629 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1630 .await
1631 );
1632 assert_eq!(
1633 cache
1634 .upsert_with_lifetime(
1635 "v",
1636 "a",
1637 json!({"fresh":true, "_seq":"100:000000001002"}),
1638 &[],
1639 PatchOrigin::Creation,
1640 Some(AccountPosition::new(100, 1002)),
1641 )
1642 .await,
1643 CacheWrite::Created
1644 );
1645 assert_eq!(
1646 cache
1647 .upsert_with_append(
1648 "v",
1649 "a",
1650 json!({"instruction":true, "_seq":"100:000000000900"}),
1651 &[],
1652 PatchOrigin::Change
1653 )
1654 .await,
1655 CacheWrite::Merged
1656 );
1657
1658 assert!(matches!(
1661 cache
1662 .upsert_with_lifetime(
1663 "v",
1664 "a",
1665 json!({"old":true, "_seq":"100:000000001001"}),
1666 &[],
1667 PatchOrigin::Change,
1668 Some(AccountPosition::new(100, 1001)),
1669 )
1670 .await,
1671 CacheWrite::Refused { .. }
1672 ));
1673 assert!(
1674 !cache
1675 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1676 .await
1677 );
1678 assert!(
1679 !cache
1680 .store_whole("v", "a", json!({"old":true, "_seq":"100:000000000900"}))
1681 .await
1682 );
1683 let row = cache.get("v", "a").await.unwrap();
1684 assert_eq!(row["fresh"], true);
1685 assert_eq!(row["instruction"], true);
1686 assert!(row.get("old").is_none());
1687
1688 cache
1689 .upsert_with_append(
1690 "v",
1691 "other",
1692 json!({"other":true}),
1693 &[],
1694 PatchOrigin::Creation,
1695 )
1696 .await;
1697 assert!(cache.get("v", "a").await.is_none());
1698 assert!(matches!(
1699 cache
1700 .upsert_with_lifetime(
1701 "v",
1702 "a",
1703 json!({"accountUpdate":true, "_seq":"100:000000001003"}),
1704 &[],
1705 PatchOrigin::Change,
1706 Some(AccountPosition::new(100, 1003)),
1707 )
1708 .await,
1709 CacheWrite::Refused { .. }
1710 ));
1711 assert!(
1712 !cache
1713 .store_whole_for_lifetime(
1714 "v",
1715 "a",
1716 row.clone(),
1717 Some(AccountPosition::new(100, 1002)),
1718 )
1719 .await
1720 );
1721 let mut current = row;
1722 current["accountUpdate"] = Value::Bool(true);
1723 current["_seq"] = Value::String("100:000000001003".into());
1724 assert!(
1725 cache
1726 .store_whole_for_lifetime("v", "a", current, Some(AccountPosition::new(100, 1003)),)
1727 .await
1728 );
1729
1730 let restored = EntityCache::new();
1733 restored.hydrate(cache.dump().await).await;
1734 restored
1735 .hydrate_lifetimes(cache.dump_lifetimes().await)
1736 .await;
1737 assert_eq!(
1738 restored
1739 .upsert_with_append(
1740 "v",
1741 "a",
1742 json!({"afterRestore":true, "_seq":"100:000000000901"}),
1743 &[],
1744 PatchOrigin::Change,
1745 )
1746 .await,
1747 CacheWrite::Merged
1748 );
1749 assert!(matches!(
1750 restored
1751 .upsert_with_lifetime(
1752 "v",
1753 "a",
1754 json!({"old":true, "_seq":"100:000000001001"}),
1755 &[],
1756 PatchOrigin::Change,
1757 Some(AccountPosition::new(100, 1001)),
1758 )
1759 .await,
1760 CacheWrite::Refused { .. }
1761 ));
1762 }
1763
1764 #[tokio::test]
1765 async fn recreation_retains_each_source_cursor_as_an_old_lifetime_barrier() {
1766 let cache = EntityCache::new();
1767 assert_eq!(
1768 cache
1769 .upsert_with_lifetime(
1770 "v",
1771 "a",
1772 json!({"lifetime":"old"}),
1773 &[],
1774 PatchOrigin::Creation,
1775 Some(AccountPosition::new(100, 9)),
1776 )
1777 .await,
1778 CacheWrite::Created
1779 );
1780 for (domain, seq, field) in [
1781 (
1782 SlotIndexDomain::Instruction,
1783 "100:000000000800",
1784 "oldInstruction",
1785 ),
1786 (SlotIndexDomain::Resolver, "100:900000000000", "oldResolver"),
1787 ] {
1788 let mut patch = json!({"_seq":seq});
1789 patch[field] = Value::Bool(true);
1790 assert_eq!(
1791 cache
1792 .upsert_with_ordering(
1793 "v",
1794 "a",
1795 patch,
1796 &[],
1797 PatchOrigin::Change,
1798 LifetimeOrdering {
1799 account_position: None,
1800 source_seq: Some(seq),
1801 source_domain: Some(domain),
1802 },
1803 )
1804 .await,
1805 CacheWrite::Merged
1806 );
1807 }
1808
1809 assert!(
1810 cache
1811 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1812 .await
1813 );
1814 assert_eq!(
1815 cache
1816 .upsert_with_lifetime(
1817 "v",
1818 "a",
1819 json!({"lifetime":"new"}),
1820 &[],
1821 PatchOrigin::Creation,
1822 Some(AccountPosition::new(100, 11)),
1823 )
1824 .await,
1825 CacheWrite::Created
1826 );
1827
1828 for (domain, seq, field) in [
1831 (
1832 SlotIndexDomain::Instruction,
1833 "100:000000000799",
1834 "staleInstruction",
1835 ),
1836 (
1837 SlotIndexDomain::Resolver,
1838 "100:899999999999",
1839 "staleResolver",
1840 ),
1841 ] {
1842 let mut patch = json!({"_seq":seq});
1843 patch[field] = Value::Bool(true);
1844 assert!(matches!(
1845 cache
1846 .upsert_with_ordering(
1847 "v",
1848 "a",
1849 patch,
1850 &[],
1851 PatchOrigin::Change,
1852 LifetimeOrdering {
1853 account_position: None,
1854 source_seq: Some(seq),
1855 source_domain: Some(domain),
1856 },
1857 )
1858 .await,
1859 CacheWrite::Refused { .. }
1860 ));
1861 }
1862
1863 assert_eq!(
1866 cache
1867 .upsert_with_ordering(
1868 "v",
1869 "a",
1870 json!({"newInstruction":true, "_seq":"100:000000000900"}),
1871 &[],
1872 PatchOrigin::Change,
1873 LifetimeOrdering {
1874 account_position: None,
1875 source_seq: Some("100:000000000900"),
1876 source_domain: Some(SlotIndexDomain::Instruction),
1877 },
1878 )
1879 .await,
1880 CacheWrite::Merged
1881 );
1882 let row = cache.get("v", "a").await.unwrap();
1883 assert_eq!(row["lifetime"], "new");
1884 assert_eq!(row["newInstruction"], true);
1885 assert!(row.get("oldInstruction").is_none());
1886 assert!(row.get("oldResolver").is_none());
1887 assert!(row.get("staleInstruction").is_none());
1888 assert!(row.get("staleResolver").is_none());
1889
1890 let restored = EntityCache::new();
1891 restored.hydrate(cache.dump().await).await;
1892 restored
1893 .hydrate_lifetimes(cache.dump_lifetimes().await)
1894 .await;
1895 assert!(matches!(
1896 restored
1897 .upsert_with_ordering(
1898 "v",
1899 "a",
1900 json!({"staleAfterRestore":true}),
1901 &[],
1902 PatchOrigin::Change,
1903 LifetimeOrdering {
1904 account_position: None,
1905 source_seq: Some("100:000000000799"),
1906 source_domain: Some(SlotIndexDomain::Instruction),
1907 },
1908 )
1909 .await,
1910 CacheWrite::Refused { .. }
1911 ));
1912 }
1913
1914 #[tokio::test]
1915 async fn markerless_sources_keep_seq_ordering_across_recreation_and_restore() {
1916 let cache = EntityCache::new();
1917 assert_eq!(
1918 cache
1919 .upsert_with_append(
1920 "v",
1921 "a",
1922 json!({"old":true,"_seq":"100:000000000010"}),
1923 &[],
1924 PatchOrigin::Creation,
1925 )
1926 .await,
1927 CacheWrite::Created
1928 );
1929 assert!(
1930 !cache
1931 .delete_ordered("v", "a", None, Some("100:000000000009"))
1932 .await
1933 );
1934 assert!(cache.get("v", "a").await.is_some());
1935 assert!(
1936 cache
1937 .delete_ordered("v", "a", None, Some("100:000000000011"))
1938 .await
1939 );
1940 assert_eq!(
1941 cache
1942 .upsert_with_append(
1943 "v",
1944 "a",
1945 json!({"fresh":true,"_seq":"100:000000000012"}),
1946 &[],
1947 PatchOrigin::Creation,
1948 )
1949 .await,
1950 CacheWrite::Created
1951 );
1952 assert!(matches!(
1953 cache
1954 .upsert_with_append(
1955 "v",
1956 "a",
1957 json!({"stale":true,"_seq":"100:000000000010"}),
1958 &[],
1959 PatchOrigin::Creation,
1960 )
1961 .await,
1962 CacheWrite::Refused { .. }
1963 ));
1964 assert!(
1965 !cache
1966 .delete_ordered("v", "a", None, Some("100:000000000011"))
1967 .await
1968 );
1969
1970 let restored = EntityCache::new();
1971 restored.hydrate(cache.dump().await).await;
1972 restored
1973 .hydrate_lifetimes(cache.dump_lifetimes().await)
1974 .await;
1975 assert!(
1976 !restored
1977 .delete_ordered("v", "a", None, Some("100:000000000011"))
1978 .await
1979 );
1980 let row = restored.get("v", "a").await.unwrap();
1981 assert_eq!(row["fresh"], true);
1982 assert!(row.get("old").is_none());
1983 assert!(row.get("stale").is_none());
1984
1985 let resync_cache = EntityCache::new();
1988 resync_cache
1989 .hydrate_lifetimes(cache.dump_lifetimes().await)
1990 .await;
1991 assert!(
1992 !resync_cache
1993 .store_whole_for_ordering(
1994 "v",
1995 "a",
1996 json!({"stale_resend":true}),
1997 None,
1998 Some("100:000000000010"),
1999 )
2000 .await
2001 );
2002 assert!(
2003 resync_cache
2004 .store_whole_for_ordering(
2005 "v",
2006 "a",
2007 json!({"fresh_resend":true}),
2008 None,
2009 Some("100:000000000012"),
2010 )
2011 .await
2012 );
2013 }
2014
2015 #[tokio::test]
2016 async fn older_source_patch_cannot_replace_newer_row_or_move_its_cursor_back() {
2017 let cache = EntityCache::new();
2018 assert_eq!(
2019 cache
2020 .upsert_with_append(
2021 "v",
2022 "a",
2023 json!({"value":"created","_seq":"100:000000000010"}),
2024 &[],
2025 PatchOrigin::Creation,
2026 )
2027 .await,
2028 CacheWrite::Created
2029 );
2030 assert_eq!(
2031 cache
2032 .upsert_with_ordering(
2033 "v",
2034 "a",
2035 json!({"value":"newer","_seq":"100:000000000012"}),
2036 &[],
2037 PatchOrigin::Change,
2038 LifetimeOrdering {
2039 account_position: None,
2040 source_seq: Some("100:000000000012"),
2041 source_domain: Some(SlotIndexDomain::Legacy),
2042 },
2043 )
2044 .await,
2045 CacheWrite::Merged
2046 );
2047 assert!(matches!(
2048 cache
2049 .upsert_with_ordering(
2050 "v",
2051 "a",
2052 json!({"value":"older","stale":true,"_seq":"100:000000000011"}),
2053 &[],
2054 PatchOrigin::Change,
2055 LifetimeOrdering {
2056 account_position: None,
2057 source_seq: Some("100:000000000011"),
2058 source_domain: Some(SlotIndexDomain::Legacy),
2059 },
2060 )
2061 .await,
2062 CacheWrite::Refused { .. }
2063 ));
2064 assert!(
2065 !cache
2066 .delete_ordered("v", "a", None, Some("100:000000000011"))
2067 .await
2068 );
2069 let row = cache.get("v", "a").await.unwrap();
2070 assert_eq!(row["value"], "newer");
2071 assert_eq!(row["_seq"], "100:000000000012");
2072 assert!(row.get("stale").is_none());
2073 }
2074
2075 #[tokio::test]
2076 async fn independent_source_recency_domains_survive_snapshot_restore() {
2077 let cache = EntityCache::new();
2078 cache
2079 .upsert_with_lifetime(
2080 "v",
2081 "a",
2082 json!({"value":"created"}),
2083 &[],
2084 PatchOrigin::Creation,
2085 Some(AccountPosition::new(100, 9)),
2086 )
2087 .await;
2088 for (domain, seq, value) in [
2089 (SlotIndexDomain::Resolver, "100:900000000000", "resolver"),
2090 (
2091 SlotIndexDomain::Instruction,
2092 "100:000000000900",
2093 "instruction",
2094 ),
2095 ] {
2096 assert_eq!(
2097 cache
2098 .upsert_with_ordering(
2099 "v",
2100 "a",
2101 json!({"value":value, "_seq":seq}),
2102 &[],
2103 PatchOrigin::Change,
2104 LifetimeOrdering {
2105 account_position: None,
2106 source_seq: Some(seq),
2107 source_domain: Some(domain),
2108 },
2109 )
2110 .await,
2111 CacheWrite::Merged
2112 );
2113 }
2114
2115 let restored = EntityCache::new();
2116 restored.hydrate(cache.dump().await).await;
2117 restored
2118 .hydrate_lifetimes(cache.dump_lifetimes().await)
2119 .await;
2120
2121 for (domain, seq) in [
2122 (SlotIndexDomain::Instruction, "100:000000000899"),
2123 (SlotIndexDomain::Resolver, "100:899999999999"),
2124 ] {
2125 assert!(matches!(
2126 restored
2127 .upsert_with_ordering(
2128 "v",
2129 "a",
2130 json!({"stale":true, "_seq":seq}),
2131 &[],
2132 PatchOrigin::Change,
2133 LifetimeOrdering {
2134 account_position: None,
2135 source_seq: Some(seq),
2136 source_domain: Some(domain),
2137 },
2138 )
2139 .await,
2140 CacheWrite::Refused { .. }
2141 ));
2142 }
2143 assert_eq!(
2144 restored.get("v", "a").await.unwrap()["value"],
2145 "instruction"
2146 );
2147 }
2148
2149 #[tokio::test]
2150 async fn explicit_current_delete_accepts_a_source_without_position() {
2151 let cache = EntityCache::new();
2152 cache
2153 .upsert_with_append(
2154 "v",
2155 "a",
2156 json!({"value":true,"_seq":"100:000000000010"}),
2157 &[],
2158 PatchOrigin::Creation,
2159 )
2160 .await;
2161
2162 assert!(cache.delete("v", "a", None).await);
2163 assert!(cache.get("v", "a").await.is_none());
2164 assert!(cache.deletion_is_current("v", "a").await);
2165 }
2166
2167 #[tokio::test]
2168 async fn evicted_live_row_does_not_make_an_earlier_delete_current() {
2169 let cache = EntityCache::with_config(EntityCacheConfig {
2170 max_entities_per_view: 1,
2171 ..Default::default()
2172 });
2173 cache
2174 .upsert_with_lifetime(
2175 "v",
2176 "a",
2177 json!({"value":true,"_seq":"100:000000000010"}),
2178 &[],
2179 PatchOrigin::Creation,
2180 Some(AccountPosition::new(100, 10)),
2181 )
2182 .await;
2183 cache
2184 .upsert_with_append("v", "b", json!({"value":true}), &[], PatchOrigin::Creation)
2185 .await;
2186
2187 assert!(cache.get("v", "a").await.is_none());
2188 assert!(!cache.deletion_is_current("v", "a").await);
2189 assert!(cache.delete_current("v", "a").await);
2190 assert!(cache.deletion_is_current("v", "a").await);
2191 }
2192
2193 #[tokio::test]
2194 async fn test_basic_upsert_and_get() {
2195 let cache = EntityCache::new();
2196
2197 cache
2198 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
2199 .await;
2200
2201 let entity = cache.get("tokens/list", "abc123").await;
2202 assert!(entity.is_some());
2203 assert_eq!(entity.unwrap()["name"], "Test Token");
2204 }
2205
2206 #[tokio::test]
2207 async fn test_deep_merge_objects() {
2208 let cache = EntityCache::new();
2209
2210 cache
2211 .upsert(
2212 "tokens/list",
2213 "abc123",
2214 json!({
2215 "id": "abc123",
2216 "metrics": {"volume": 100}
2217 }),
2218 )
2219 .await;
2220
2221 cache
2222 .upsert(
2223 "tokens/list",
2224 "abc123",
2225 json!({
2226 "metrics": {"trades": 50}
2227 }),
2228 )
2229 .await;
2230
2231 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2232 assert_eq!(entity["id"], "abc123");
2233 assert_eq!(entity["metrics"]["volume"], 100);
2234 assert_eq!(entity["metrics"]["trades"], 50);
2235 }
2236
2237 #[tokio::test]
2238 async fn test_array_append() {
2239 let cache = EntityCache::new();
2240
2241 cache
2242 .upsert(
2243 "tokens/list",
2244 "abc123",
2245 json!({
2246 "events": [{"type": "buy", "amount": 100}]
2247 }),
2248 )
2249 .await;
2250
2251 cache
2252 .upsert_with_append(
2253 "tokens/list",
2254 "abc123",
2255 json!({
2256 "events": [{"type": "sell", "amount": 50}]
2257 }),
2258 &["events".to_string()],
2259 PatchOrigin::Unknown,
2260 )
2261 .await;
2262
2263 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2264 let events = entity["events"].as_array().unwrap();
2265 assert_eq!(events.len(), 2);
2266 assert_eq!(events[0]["type"], "buy");
2267 assert_eq!(events[1]["type"], "sell");
2268 }
2269
2270 #[tokio::test]
2271 async fn test_array_lru_eviction() {
2272 let config = EntityCacheConfig {
2273 max_entities_per_view: 1000,
2274 max_array_length: 3,
2275 ..Default::default()
2276 };
2277 let cache = EntityCache::with_config(config);
2278
2279 cache
2280 .upsert(
2281 "tokens/list",
2282 "abc123",
2283 json!({
2284 "events": [
2285 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
2286 ]
2287 }),
2288 )
2289 .await;
2290
2291 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2292 let events = entity["events"].as_array().unwrap();
2293
2294 assert_eq!(events.len(), 3);
2295 assert_eq!(events[0]["id"], 3);
2296 assert_eq!(events[1]["id"], 4);
2297 assert_eq!(events[2]["id"], 5);
2298 }
2299
2300 #[tokio::test]
2301 async fn test_array_append_with_lru() {
2302 let config = EntityCacheConfig {
2303 max_entities_per_view: 1000,
2304 max_array_length: 3,
2305 ..Default::default()
2306 };
2307 let cache = EntityCache::with_config(config);
2308
2309 cache
2310 .upsert(
2311 "tokens/list",
2312 "abc123",
2313 json!({
2314 "events": [{"id": 1}, {"id": 2}]
2315 }),
2316 )
2317 .await;
2318
2319 cache
2320 .upsert_with_append(
2321 "tokens/list",
2322 "abc123",
2323 json!({
2324 "events": [{"id": 3}, {"id": 4}]
2325 }),
2326 &["events".to_string()],
2327 PatchOrigin::Unknown,
2328 )
2329 .await;
2330
2331 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2332 let events = entity["events"].as_array().unwrap();
2333
2334 assert_eq!(events.len(), 3);
2336 assert_eq!(events[0]["id"], 2);
2337 assert_eq!(events[1]["id"], 3);
2338 assert_eq!(events[2]["id"], 4);
2339 }
2340
2341 #[tokio::test]
2342 async fn test_entity_lru_eviction() {
2343 let config = EntityCacheConfig {
2344 max_entities_per_view: 2,
2345 max_array_length: 100,
2346 ..Default::default()
2347 };
2348 let cache = EntityCache::with_config(config);
2349
2350 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2351 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2352 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
2353
2354 assert!(cache.get("tokens/list", "key1").await.is_none());
2355 assert!(cache.get("tokens/list", "key2").await.is_some());
2356 assert!(cache.get("tokens/list", "key3").await.is_some());
2357 }
2358
2359 #[tokio::test]
2360 async fn a_patch_for_an_evicted_key_is_refused_not_stored() {
2361 let cache = EntityCache::with_config(EntityCacheConfig {
2362 max_entities_per_view: 2,
2363 ..Default::default()
2364 });
2365 for id in 1..=3 {
2366 assert_eq!(
2367 cache
2368 .upsert(
2369 "tokens/list",
2370 &format!("key{id}"),
2371 json!({"id": id, "n": 0})
2372 )
2373 .await,
2374 CacheWrite::Created
2375 );
2376 }
2377 assert_eq!(
2379 cache.upsert("tokens/list", "key1", json!({"n": 5})).await,
2380 CacheWrite::Refused {
2381 patch: json!({"n": 5})
2382 }
2383 );
2384 assert!(cache.get("tokens/list", "key1").await.is_none());
2385 assert_eq!(
2386 cache.upsert("tokens/list", "key3", json!({"n": 1})).await,
2387 CacheWrite::Merged
2388 );
2389
2390 cache.remove("tokens/list", "key1").await;
2392 assert_eq!(
2393 cache
2394 .upsert("tokens/list", "key1", json!({"id": 1, "n": 6}))
2395 .await,
2396 CacheWrite::Created
2397 );
2398 }
2399
2400 #[tokio::test]
2401 async fn a_whole_entity_ends_an_eviction() {
2402 let cache = EntityCache::with_config(EntityCacheConfig {
2403 max_entities_per_view: 1,
2404 ..Default::default()
2405 });
2406 cache.upsert("v", "a", json!({"id": "a", "n": 0})).await;
2407 cache.upsert("v", "b", json!({"id": "b", "n": 0})).await;
2408 assert!(matches!(
2409 cache.upsert("v", "a", json!({"n": 1})).await,
2410 CacheWrite::Refused { .. }
2411 ));
2412 cache
2413 .store_whole("v", "a", json!({"id": "a", "n": 1}))
2414 .await;
2415 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2416 assert_eq!(cache.len("v").await, 1, "still bounded: b made room");
2417 assert_eq!(
2418 cache.upsert("v", "a", json!({"n": 2})).await,
2419 CacheWrite::Merged
2420 );
2421 }
2422
2423 #[tokio::test]
2424 async fn the_evicted_key_memory_is_bounded() {
2425 let cache = EntityCache::with_config(EntityCacheConfig {
2426 max_entities_per_view: 1,
2427 ..Default::default()
2428 });
2429 let remembered = EVICTED_KEYS_PER_CACHED_ENTITY;
2430 for key in 0..=remembered + 1 {
2431 cache
2432 .upsert("v", &key.to_string(), json!({"id": key}))
2433 .await;
2434 }
2435 assert_eq!(
2438 cache.upsert("v", "0", json!({"id": 0})).await,
2439 CacheWrite::Created
2440 );
2441 assert!(matches!(
2442 cache
2443 .upsert("v", &remembered.to_string(), json!({"n": 1}))
2444 .await,
2445 CacheWrite::Refused { .. }
2446 ));
2447 }
2448
2449 async fn write(
2450 cache: &EntityCache,
2451 key: &str,
2452 patch: Value,
2453 origin: PatchOrigin,
2454 ) -> CacheWrite {
2455 cache.upsert_with_append("v", key, patch, &[], origin).await
2456 }
2457
2458 #[tokio::test]
2461 async fn an_unmarked_patch_for_a_key_not_held_is_refused_however_old() {
2462 let cache = EntityCache::with_config(EntityCacheConfig {
2463 max_entities_per_view: 1,
2464 ..Default::default()
2465 });
2466 let evictions = EVICTED_KEYS_PER_CACHED_ENTITY * 4;
2467 for key in 0..=evictions {
2468 assert_eq!(
2469 write(
2470 &cache,
2471 &key.to_string(),
2472 json!({"id": key}),
2473 PatchOrigin::Creation
2474 )
2475 .await,
2476 CacheWrite::Created,
2477 "a creation is stored as soon as it arrives"
2478 );
2479 }
2480 assert_eq!(
2482 write(&cache, "0", json!({"n": 1}), PatchOrigin::Change).await,
2483 CacheWrite::Refused {
2484 patch: json!({"n": 1})
2485 }
2486 );
2487 assert!(
2488 matches!(
2489 write(&cache, "never", json!({"n": 1}), PatchOrigin::Change).await,
2490 CacheWrite::Refused { .. }
2491 ),
2492 "nor does it matter whether the view ever held the key"
2493 );
2494 assert_eq!(cache.get("v", "0").await, None);
2495 assert_eq!(
2496 write(
2497 &cache,
2498 &evictions.to_string(),
2499 json!({"n": 1}),
2500 PatchOrigin::Change
2501 )
2502 .await,
2503 CacheWrite::Merged
2504 );
2505 assert_eq!(
2507 write(&cache, "0", json!({"id": 0, "n": 2}), PatchOrigin::Creation).await,
2508 CacheWrite::Created
2509 );
2510 assert_eq!(cache.get("v", "0").await, Some(json!({"id": 0, "n": 2})));
2511 }
2512
2513 #[tokio::test]
2516 async fn a_creation_for_a_key_held_replaces() {
2517 let cache = EntityCache::new();
2518 write(
2519 &cache,
2520 "a",
2521 json!({"id": "a", "old": 1}),
2522 PatchOrigin::Creation,
2523 )
2524 .await;
2525 assert_eq!(
2526 write(
2527 &cache,
2528 "a",
2529 json!({"id": "a", "n": 1}),
2530 PatchOrigin::Creation
2531 )
2532 .await,
2533 CacheWrite::Created
2534 );
2535 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2536 }
2537
2538 async fn remembers_evictions(cache: &EntityCache, view_id: &str) -> bool {
2539 cache
2540 .caches
2541 .read()
2542 .await
2543 .get(view_id)
2544 .is_some_and(|view| view.evicted.is_some())
2545 }
2546
2547 #[tokio::test]
2551 async fn a_view_fed_marked_creations_keeps_no_eviction_memory() {
2552 let cache = EntityCache::with_config(EntityCacheConfig {
2553 max_entities_per_view: 1,
2554 ..Default::default()
2555 });
2556 cache.remember_evicted("v", ["restored".to_string()]).await;
2557 assert!(remembers_evictions(&cache, "v").await);
2558
2559 write(&cache, "a", json!({"id": "a"}), PatchOrigin::Creation).await;
2560 assert!(!remembers_evictions(&cache, "v").await);
2561 write(&cache, "b", json!({"id": "b"}), PatchOrigin::Creation).await;
2562 assert!(!remembers_evictions(&cache, "v").await, "a was evicted");
2563 assert!(matches!(
2564 write(&cache, "restored", json!({"n": 1}), PatchOrigin::Change).await,
2565 CacheWrite::Refused { .. }
2566 ));
2567 assert!(matches!(
2568 write(&cache, "a", json!({"n": 1}), PatchOrigin::Change).await,
2569 CacheWrite::Refused { .. }
2570 ));
2571 }
2572
2573 #[tokio::test]
2574 async fn remembered_keys_are_refused_unless_held() {
2575 let cache = EntityCache::new();
2576 cache.upsert("tokens/list", "held", json!({"id": 1})).await;
2577 cache
2578 .remember_evicted("tokens/list", ["held".to_string(), "gone".to_string()])
2579 .await;
2580 assert_eq!(
2581 cache.upsert("tokens/list", "held", json!({"n": 1})).await,
2582 CacheWrite::Merged
2583 );
2584 assert!(matches!(
2585 cache.upsert("tokens/list", "gone", json!({"n": 1})).await,
2586 CacheWrite::Refused { .. }
2587 ));
2588 assert_eq!(
2590 cache.upsert("tokens/state", "gone", json!({"id": 2})).await,
2591 CacheWrite::Created
2592 );
2593 }
2594
2595 #[tokio::test]
2596 async fn test_get_all() {
2597 let cache = EntityCache::new();
2598
2599 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2600 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2601
2602 let all = cache.get_all("tokens/list").await;
2603 assert_eq!(all.len(), 2);
2604 }
2605
2606 #[tokio::test]
2607 async fn remove_is_scoped_to_one_entity() {
2608 let cache = EntityCache::new();
2609 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
2610 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
2611
2612 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
2613 assert!(cache.get("tokens/list", "one").await.is_none());
2614 assert!(cache.get("tokens/list", "two").await.is_some());
2615 }
2616
2617 #[tokio::test]
2618 async fn test_separate_views() {
2619 let cache = EntityCache::new();
2620
2621 cache
2622 .upsert("tokens/list", "key1", json!({"type": "token"}))
2623 .await;
2624 cache
2625 .upsert("games/list", "key1", json!({"type": "game"}))
2626 .await;
2627
2628 let token = cache.get("tokens/list", "key1").await.unwrap();
2629 let game = cache.get("games/list", "key1").await.unwrap();
2630
2631 assert_eq!(token["type"], "token");
2632 assert_eq!(game["type"], "game");
2633 }
2634
2635 #[test]
2636 fn test_deep_merge_with_append() {
2637 let mut base = json!({
2638 "a": 1,
2639 "b": {"c": 2},
2640 "arr": [1, 2]
2641 });
2642
2643 let patch = json!({
2644 "b": {"d": 3},
2645 "arr": [3],
2646 "e": 4
2647 });
2648
2649 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
2650
2651 assert_eq!(base["a"], 1);
2652 assert_eq!(base["b"]["c"], 2);
2653 assert_eq!(base["b"]["d"], 3);
2654 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
2655 assert_eq!(base["e"], 4);
2656 }
2657
2658 #[test]
2659 fn test_deep_merge_replace_array() {
2660 let mut base = json!({
2661 "arr": [1, 2, 3]
2662 });
2663
2664 let patch = json!({
2665 "arr": [4, 5]
2666 });
2667
2668 deep_merge_with_append(&mut base, patch, &[], 100);
2669
2670 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
2671 assert_eq!(base["arr"][0], 4);
2672 assert_eq!(base["arr"][1], 5);
2673 }
2674
2675 #[test]
2676 fn test_deep_merge_nested_append() {
2677 let mut base = json!({
2678 "stats": {"events": [1, 2]}
2679 });
2680
2681 let patch = json!({
2682 "stats": {"events": [3]}
2683 });
2684
2685 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
2686
2687 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
2688 }
2689
2690 #[test]
2691 fn test_snapshot_config_defaults() {
2692 let cache = EntityCache::new();
2693 let config = cache.snapshot_config();
2694
2695 assert_eq!(config.initial_batch_size, 50);
2696 assert_eq!(config.subsequent_batch_size, 100);
2697 }
2698
2699 #[test]
2700 fn test_snapshot_config_custom() {
2701 let config = EntityCacheConfig {
2702 initial_snapshot_batch_size: 25,
2703 subsequent_snapshot_batch_size: 75,
2704 ..Default::default()
2705 };
2706 let cache = EntityCache::with_config(config);
2707 let snapshot_config = cache.snapshot_config();
2708
2709 assert_eq!(snapshot_config.initial_batch_size, 25);
2710 assert_eq!(snapshot_config.subsequent_batch_size, 75);
2711 }
2712
2713 #[tokio::test]
2714 async fn test_get_after() {
2715 let cache = EntityCache::new();
2716
2717 cache
2719 .upsert(
2720 "tokens/list",
2721 "key1",
2722 json!({"id": 1, "_seq": "100:000000000001"}),
2723 )
2724 .await;
2725 cache
2726 .upsert(
2727 "tokens/list",
2728 "key2",
2729 json!({"id": 2, "_seq": "100:000000000002"}),
2730 )
2731 .await;
2732 cache
2733 .upsert(
2734 "tokens/list",
2735 "key3",
2736 json!({"id": 3, "_seq": "100:000000000003"}),
2737 )
2738 .await;
2739 cache
2740 .upsert(
2741 "tokens/list",
2742 "key4",
2743 json!({"id": 4, "_seq": "101:000000000001"}),
2744 )
2745 .await;
2746
2747 let after = cache
2749 .get_after("tokens/list", "100:000000000002", None)
2750 .await;
2751
2752 assert_eq!(after.len(), 2);
2754 assert_eq!(after[0].0, "key3");
2755 assert_eq!(after[1].0, "key4");
2756 }
2757
2758 #[tokio::test]
2759 async fn test_get_after_with_limit() {
2760 let cache = EntityCache::new();
2761
2762 cache
2764 .upsert(
2765 "tokens/list",
2766 "key1",
2767 json!({"id": 1, "_seq": "100:000000000001"}),
2768 )
2769 .await;
2770 cache
2771 .upsert(
2772 "tokens/list",
2773 "key2",
2774 json!({"id": 2, "_seq": "100:000000000002"}),
2775 )
2776 .await;
2777 cache
2778 .upsert(
2779 "tokens/list",
2780 "key3",
2781 json!({"id": 3, "_seq": "100:000000000003"}),
2782 )
2783 .await;
2784
2785 let after = cache
2787 .get_after("tokens/list", "100:000000000000", Some(2))
2788 .await;
2789
2790 assert_eq!(after.len(), 2);
2792 assert_eq!(after[0].0, "key1");
2793 assert_eq!(after[1].0, "key2");
2794 }
2795
2796 #[tokio::test]
2797 async fn test_get_after_empty_result() {
2798 let cache = EntityCache::new();
2799
2800 cache
2801 .upsert(
2802 "tokens/list",
2803 "key1",
2804 json!({"id": 1, "_seq": "100:000000000001"}),
2805 )
2806 .await;
2807
2808 let after = cache
2810 .get_after("tokens/list", "999:000000000000", None)
2811 .await;
2812
2813 assert!(after.is_empty());
2814 }
2815
2816 #[tokio::test]
2817 async fn test_get_after_missing_seq() {
2818 let cache = EntityCache::new();
2819
2820 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2822
2823 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
2825
2826 assert!(after.is_empty());
2827 }
2828
2829 fn versions<'a>(views: &[(&'a str, &str)]) -> Vec<ViewVersion<'a>> {
2830 views
2831 .iter()
2832 .map(|(view_id, version)| ViewVersion {
2833 view_id,
2834 version: Some(json!(version)),
2835 })
2836 .collect()
2837 }
2838
2839 const VIEWS: [&str; 3] = ["t/list", "t/state", "t/append"];
2840
2841 async fn shared(cache: &EntityCache, key: &str) -> Vec<SharedEntity> {
2842 let mut entities = Vec::new();
2843 for view in VIEWS {
2844 entities.push(cache.get_shared(view, key).await.unwrap());
2845 }
2846 entities
2847 }
2848
2849 async fn write_all(
2850 cache: &EntityCache,
2851 key: &str,
2852 patch: Value,
2853 stamps: [&str; 3],
2854 origin: PatchOrigin,
2855 ) -> Vec<CacheWrite> {
2856 let views: Vec<_> = VIEWS.into_iter().zip(stamps).collect();
2857 cache
2858 .upsert_views(
2859 key,
2860 patch,
2861 &versions(&views),
2862 &["events".to_string()],
2863 origin,
2864 LifetimeOrdering::default(),
2865 )
2866 .await
2867 }
2868
2869 #[tokio::test]
2873 async fn views_written_together_share_one_copy() {
2874 let cache = EntityCache::new();
2875 let writes = write_all(
2876 &cache,
2877 "a",
2878 json!({"id": "a", "events": [1]}),
2879 ["e:1", "e:2", "e:3"],
2880 PatchOrigin::Creation,
2881 )
2882 .await;
2883 assert_eq!(writes, vec![CacheWrite::Created; 3]);
2884 let [list, state, append] =
2885 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
2886 assert!(list.shares_fields_with(&state) && list.shares_fields_with(&append));
2887 assert_eq!(Arc::strong_count(list.fields()), 6);
2889 assert_eq!(
2890 cache.get("t/state", "a").await,
2891 Some(json!({"id": "a", "events": [1], "_version": "e:2"}))
2892 );
2893
2894 let writes = write_all(
2895 &cache,
2896 "a",
2897 json!({"n": 1, "events": [2]}),
2898 ["e:4", "e:5", "e:6"],
2899 PatchOrigin::Unknown,
2900 )
2901 .await;
2902 assert_eq!(writes, vec![CacheWrite::Merged; 3]);
2903 let merged = shared(&cache, "a").await;
2904 assert!(
2905 merged[0].shares_fields_with(&merged[1]) && merged[0].shares_fields_with(&merged[2])
2906 );
2907 for (view, version) in VIEWS.into_iter().zip(["e:4", "e:5", "e:6"]) {
2908 assert_eq!(
2909 cache.get(view, "a").await,
2910 Some(json!({"id": "a", "n": 1, "events": [1, 2], "_version": version})),
2911 "{view}"
2912 );
2913 }
2914 assert_eq!(
2916 list.to_value(),
2917 json!({"id": "a", "events": [1], "_version": "e:1"})
2918 );
2919 assert!(!list.shares_fields_with(&merged[0]));
2920 }
2921
2922 #[tokio::test]
2925 async fn a_merge_nobody_else_holds_happens_in_place() {
2926 let cache = EntityCache::new();
2927 write_all(
2928 &cache,
2929 "a",
2930 json!({"id": "a"}),
2931 ["e:1", "e:2", "e:3"],
2932 PatchOrigin::Creation,
2933 )
2934 .await;
2935 let before = Arc::as_ptr(cache.get_shared("t/list", "a").await.unwrap().fields());
2936 write_all(
2937 &cache,
2938 "a",
2939 json!({"n": 1}),
2940 ["e:4", "e:5", "e:6"],
2941 PatchOrigin::Unknown,
2942 )
2943 .await;
2944 let after = cache.get_shared("t/list", "a").await.unwrap();
2945 assert_eq!(Arc::as_ptr(after.fields()), before);
2948 assert_eq!(Arc::strong_count(after.fields()), 4);
2949 assert_eq!(after["n"], 1);
2950 }
2951
2952 #[tokio::test]
2955 async fn a_patch_to_one_view_does_not_reach_another() {
2956 let cache = EntityCache::new();
2957 write_all(
2958 &cache,
2959 "a",
2960 json!({"id": "a", "nested": {"n": 0}}),
2961 ["e:1", "e:2", "e:3"],
2962 PatchOrigin::Creation,
2963 )
2964 .await;
2965 assert_eq!(
2966 cache
2967 .upsert_with_append(
2968 "t/list",
2969 "a",
2970 json!({"nested": {"n": 1}, "_version": "e:4"}),
2971 &[],
2972 PatchOrigin::Unknown,
2973 )
2974 .await,
2975 CacheWrite::Merged
2976 );
2977 assert_eq!(
2978 cache.get("t/list", "a").await,
2979 Some(json!({"id": "a", "nested": {"n": 1}, "_version": "e:4"}))
2980 );
2981 for (view, version) in [("t/state", "e:2"), ("t/append", "e:3")] {
2982 assert_eq!(
2983 cache.get(view, "a").await,
2984 Some(json!({"id": "a", "nested": {"n": 0}, "_version": version})),
2985 "{view}"
2986 );
2987 }
2988 let [list, state, append] =
2989 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
2990 assert!(!list.shares_fields_with(&state));
2991 assert!(state.shares_fields_with(&append));
2992 }
2993
2994 #[tokio::test]
2997 async fn a_view_that_refuses_a_patch_keeps_its_copy() {
2998 let cache = EntityCache::new();
2999 write_all(
3000 &cache,
3001 "a",
3002 json!({"id": "a", "n": 0}),
3003 ["e:1", "e:2", "e:3"],
3004 PatchOrigin::Creation,
3005 )
3006 .await;
3007 assert!(cache.delete_current("t/state", "a").await);
3009 let writes = write_all(
3010 &cache,
3011 "a",
3012 json!({"n": 1}),
3013 ["e:4", "e:5", "e:6"],
3014 PatchOrigin::Change,
3015 )
3016 .await;
3017 assert_eq!(writes[0], CacheWrite::Merged);
3018 assert_eq!(
3019 writes[1],
3020 CacheWrite::Refused {
3021 patch: json!({"n": 1, "_version": "e:5"})
3022 }
3023 );
3024 assert_eq!(writes[2], CacheWrite::Merged);
3025 assert_eq!(cache.get("t/state", "a").await, None);
3026 let list = cache.get_shared("t/list", "a").await.unwrap();
3027 let append = cache.get_shared("t/append", "a").await.unwrap();
3028 assert!(list.shares_fields_with(&append));
3029 assert_eq!(
3030 append.to_value(),
3031 json!({"id": "a", "n": 1, "_version": "e:6"})
3032 );
3033 }
3034
3035 #[tokio::test]
3038 async fn a_restore_shares_the_copies_it_saved_per_view() {
3039 let cache = EntityCache::new();
3040 write_all(
3041 &cache,
3042 "a",
3043 json!({"id": "a", "events": [1, 2]}),
3044 ["e:1", "e:2", "e:3"],
3045 PatchOrigin::Creation,
3046 )
3047 .await;
3048 let mut dump = cache.dump().await;
3049 dump.push((
3052 "other/list".to_string(),
3053 vec![("a".to_string(), json!({"id": "a", "events": [1, 2]}))],
3054 ));
3055 dump.sort_by(|left, right| left.0.cmp(&right.0));
3056
3057 let restored = EntityCache::new();
3058 restored.hydrate(dump.clone()).await;
3059 let other = restored.get_shared("other/list", "a").await.unwrap();
3060 let copies = shared(&restored, "a").await;
3061 assert!(!other.shares_fields_with(&copies[0]));
3062 assert!(
3063 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3064 );
3065 let mut again = restored.dump().await;
3066 again.sort_by(|left, right| left.0.cmp(&right.0));
3067 assert_eq!(again, dump);
3068 }
3069
3070 #[tokio::test]
3072 async fn a_whole_entity_stored_in_several_views_is_one_copy() {
3073 let cache = EntityCache::new();
3074 let views = versions(&[("t/list", "e:1"), ("t/state", "e:2"), ("t/append", "e:3")]);
3075 let stored = cache
3076 .store_whole_views("a", json!({"id": "a"}), &views, None, None)
3077 .await;
3078 assert_eq!(stored, vec![true; 3]);
3079 let copies = shared(&cache, "a").await;
3080 assert!(
3081 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3082 );
3083 assert_eq!(
3084 cache.get("t/append", "a").await,
3085 Some(json!({"id": "a", "_version": "e:3"}))
3086 );
3087 }
3088
3089 #[test]
3092 fn a_shared_merge_matches_the_whole_value_merge() {
3093 let cases = [
3094 (
3095 json!({"a": 1, "_version": "e:1"}),
3096 json!({"b": 2, "_version": "e:2"}),
3097 ),
3098 (json!({"a": 1, "_version": "e:1"}), json!({"b": 2})),
3099 (json!({"a": 1, "_version": "e:1"}), json!([1, 2])),
3100 (json!([1, 2]), json!([3])),
3101 (json!([1, 2]), json!({"a": 1, "_version": "e:2"})),
3102 (json!("text"), json!(5)),
3103 (
3104 json!({"_version": "e:1", "list": [1]}),
3105 json!({"list": [2, 3, 4]}),
3106 ),
3107 ];
3108 for (base, patch) in cases {
3109 let append = ["list".to_string()];
3110 let mut expected = base.clone();
3111 deep_merge_with_append(&mut expected, patch.clone(), &append, 2);
3112 let mut entity = SharedEntity::new(base.clone());
3113 let other = entity.clone();
3114 merge_shared(&mut entity, patch.clone(), &append, 2);
3115 assert_eq!(entity.to_value(), expected, "{base} <- {patch}");
3116 assert_eq!(other.to_value(), base, "the other holder's copy");
3117 }
3118 }
3119}