1use crate::mutation_batch::SlotIndexDomain;
8use arete_interpreter::AccountPosition;
9use lru::LruCache;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::BTreeMap;
13use std::collections::HashMap;
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17const DEFAULT_MAX_ENTITIES_PER_VIEW: usize = 500;
18const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
19const DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE: usize = 50;
20const DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE: usize = 100;
21const EVICTED_KEYS_PER_CACHED_ENTITY: usize = 8;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum PatchOrigin {
40 Creation,
44 Change,
48 Unknown,
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum CacheWrite {
56 Merged,
58 Created,
61 Refused { patch: Value },
66}
67
68struct ViewEntries {
75 max_entities: usize,
76 entities: LruCache<String, Value>,
77 evicted: Option<LruCache<String, ()>>,
81 creations_marked: bool,
84 lifetimes: LruCache<String, EntityLifetimeCheckpoint>,
88}
89
90impl ViewEntries {
91 fn new(max_entities: usize) -> Self {
92 assert!(max_entities > 0, "max_entities_per_view must be > 0");
93 Self {
94 max_entities,
95 entities: LruCache::unbounded(),
96 evicted: None,
97 creations_marked: false,
98 lifetimes: LruCache::unbounded(),
99 }
100 }
101
102 fn mark_creations(&mut self) {
105 self.creations_marked = true;
106 self.evicted = None;
107 }
108
109 fn store(&mut self, key: String, entity: Value) {
111 self.forget_evicted(&key);
112 self.entities.put(key, entity);
113 while self.entities.len() > self.max_entities {
114 match self.entities.pop_lru() {
115 Some((evicted, _)) => self.remember_evicted(evicted),
116 None => break,
117 }
118 }
119 }
120
121 fn remember_evicted(&mut self, key: String) {
122 if self.creations_marked {
123 return;
124 }
125 let capacity = self
126 .max_entities
127 .saturating_mul(EVICTED_KEYS_PER_CACHED_ENTITY);
128 let evicted = self.evicted.get_or_insert_with(LruCache::unbounded);
129 evicted.put(key, ());
130 while evicted.len() > capacity {
131 evicted.pop_lru();
132 }
133 }
134
135 fn forget_evicted(&mut self, key: &str) {
136 if let Some(evicted) = self.evicted.as_mut() {
137 evicted.pop(key);
138 }
139 }
140
141 fn was_evicted(&mut self, key: &str) -> bool {
144 self.evicted
145 .as_mut()
146 .is_some_and(|evicted| evicted.get(key).is_some())
147 }
148
149 fn accepts(
150 &self,
151 key: &str,
152 creation: bool,
153 account_position: Option<AccountPosition>,
154 source_seq: Option<&str>,
155 source_domain: Option<SlotIndexDomain>,
156 ) -> bool {
157 if let Some(checkpoint) = self.lifetimes.peek(key) {
158 match (checkpoint.account_position, account_position) {
159 (Some(previous), Some(incoming)) if incoming <= previous => return false,
160 (Some(_), None) if creation || checkpoint.deleted => return false,
163 _ => {}
164 }
165 if checkpoint.deleted && !creation {
166 return false;
167 }
168 }
169
170 if account_position.is_none() && source_domain.is_some() && source_seq.is_some() {
171 return self.accepts_source_ordering(key, source_seq, source_domain, !creation);
172 }
173 true
174 }
175
176 fn latest_source_seq<'a>(
177 &'a self,
178 key: &str,
179 source_domain: Option<SlotIndexDomain>,
180 ) -> Option<&'a str> {
181 let checkpoint = self.lifetimes.peek(key);
182 if checkpoint.is_some_and(|checkpoint| checkpoint.account_position.is_some()) {
186 return checkpoint.and_then(|checkpoint| checkpoint.source_seq(source_domain));
187 }
188 let checkpoint = checkpoint.and_then(|checkpoint| checkpoint.source_seq(source_domain));
189 if source_domain != Some(SlotIndexDomain::Legacy) {
194 return checkpoint;
195 }
196 let entity = self
197 .entities
198 .peek(key)
199 .and_then(|entity| entity.get("_seq"))
200 .and_then(Value::as_str);
201 match (checkpoint, entity) {
202 (Some(left), Some(right)) if cmp_seq(left, right).is_lt() => Some(right),
203 (Some(left), _) => Some(left),
204 (None, entity) => entity,
205 }
206 }
207
208 fn accepts_source_ordering(
209 &self,
210 key: &str,
211 incoming: Option<&str>,
212 source_domain: Option<SlotIndexDomain>,
213 allow_equal: bool,
214 ) -> bool {
215 match (self.latest_source_seq(key, source_domain), incoming) {
216 (Some(previous), Some(incoming)) => {
217 let ordering = cmp_seq(incoming, previous);
218 ordering.is_gt() || (allow_equal && ordering.is_eq())
219 }
220 (Some(_), None) => false,
223 (None, _) => true,
224 }
225 }
226
227 fn trim_lifetimes(&mut self) {
228 while self.lifetimes.len()
229 > self
230 .max_entities
231 .saturating_mul(EVICTED_KEYS_PER_CACHED_ENTITY)
232 {
233 self.lifetimes.pop_lru();
234 }
235 }
236}
237
238#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(rename_all = "camelCase")]
241pub struct EntityLifetimeCheckpoint {
242 #[serde(skip_serializing_if = "Option::is_none")]
243 pub account_position: Option<AccountPosition>,
244 #[serde(skip_serializing_if = "Option::is_none")]
247 pub source_seq: Option<String>,
248 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
251 pub source_sequences: BTreeMap<SlotIndexDomain, String>,
252 pub deleted: bool,
253}
254
255impl EntityLifetimeCheckpoint {
256 fn source_seq(&self, source_domain: Option<SlotIndexDomain>) -> Option<&str> {
257 match source_domain {
258 Some(SlotIndexDomain::Legacy) | None => self
259 .source_sequences
260 .get(&SlotIndexDomain::Legacy)
261 .map(String::as_str)
262 .or(self.source_seq.as_deref()),
263 Some(domain) => self.source_sequences.get(&domain).map(String::as_str),
264 }
265 }
266}
267
268pub type EntityLifetimes = Vec<(String, String, EntityLifetimeCheckpoint)>;
270
271#[derive(Debug, Clone, Copy, Default)]
273pub struct LifetimeOrdering<'a> {
274 pub account_position: Option<AccountPosition>,
275 pub source_seq: Option<&'a str>,
276 pub source_domain: Option<SlotIndexDomain>,
279}
280
281pub type EntityTombstones = Vec<(String, String, Option<String>)>;
284
285pub fn cmp_seq(a: &str, b: &str) -> std::cmp::Ordering {
289 fn parse(s: &str) -> (u64, u64) {
290 let mut parts = s.splitn(2, ':');
291 let slot = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
292 let offset = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
293 (slot, offset)
294 }
295 parse(a).cmp(&parse(b))
296}
297
298fn account_position_from_seq(seq: &str) -> Option<AccountPosition> {
299 let mut parts = seq.splitn(2, ':');
300 Some(AccountPosition::new(
301 parts.next()?.parse().ok()?,
302 parts.next()?.parse().ok()?,
303 ))
304}
305
306#[derive(Debug, Clone)]
308pub struct EntityCacheConfig {
309 pub max_entities_per_view: usize,
311 pub max_array_length: usize,
313 pub initial_snapshot_batch_size: usize,
315 pub subsequent_snapshot_batch_size: usize,
317}
318
319impl Default for EntityCacheConfig {
320 fn default() -> Self {
321 Self {
322 max_entities_per_view: DEFAULT_MAX_ENTITIES_PER_VIEW,
323 max_array_length: DEFAULT_MAX_ARRAY_LENGTH,
324 initial_snapshot_batch_size: DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE,
325 subsequent_snapshot_batch_size: DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE,
326 }
327 }
328}
329
330#[derive(Clone)]
359pub struct EntityCache {
360 caches: Arc<RwLock<HashMap<String, ViewEntries>>>,
362 config: EntityCacheConfig,
363}
364
365impl EntityCache {
366 pub fn new() -> Self {
368 Self::with_config(EntityCacheConfig::default())
369 }
370
371 pub fn with_config(config: EntityCacheConfig) -> Self {
373 Self {
374 caches: Arc::new(RwLock::new(HashMap::new())),
375 config,
376 }
377 }
378
379 pub fn max_entities_per_view(&self) -> usize {
384 self.config.max_entities_per_view
385 }
386
387 pub async fn upsert(&self, view_id: &str, key: &str, patch: Value) -> CacheWrite {
390 self.upsert_with_append(view_id, key, patch, &[], PatchOrigin::Unknown)
391 .await
392 }
393
394 pub async fn upsert_with_append(
398 &self,
399 view_id: &str,
400 key: &str,
401 patch: Value,
402 append_paths: &[String],
403 origin: PatchOrigin,
404 ) -> CacheWrite {
405 self.upsert_with_lifetime(view_id, key, patch, append_paths, origin, None)
406 .await
407 }
408
409 pub async fn upsert_with_lifetime(
413 &self,
414 view_id: &str,
415 key: &str,
416 patch: Value,
417 append_paths: &[String],
418 origin: PatchOrigin,
419 account_position: Option<AccountPosition>,
420 ) -> CacheWrite {
421 let source_seq = patch.get("_seq").and_then(Value::as_str).map(str::to_owned);
422 self.upsert_with_ordering(
423 view_id,
424 key,
425 patch,
426 append_paths,
427 origin,
428 LifetimeOrdering {
429 account_position,
430 source_seq: source_seq.as_deref(),
431 source_domain: (origin == PatchOrigin::Creation).then_some(SlotIndexDomain::Legacy),
435 },
436 )
437 .await
438 }
439
440 pub async fn upsert_with_ordering(
444 &self,
445 view_id: &str,
446 key: &str,
447 patch: Value,
448 append_paths: &[String],
449 origin: PatchOrigin,
450 ordering: LifetimeOrdering<'_>,
451 ) -> CacheWrite {
452 let LifetimeOrdering {
453 account_position,
454 source_seq,
455 source_domain,
456 } = ordering;
457 let mut caches = self.caches.write().await;
458
459 let view = caches
460 .entry(view_id.to_string())
461 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
462 if origin != PatchOrigin::Unknown && !view.creations_marked {
463 view.mark_creations();
464 }
465
466 let max_array_length = self.config.max_array_length;
467
468 let creation = origin == PatchOrigin::Creation;
469 if !view.accepts(key, creation, account_position, source_seq, source_domain) {
470 return CacheWrite::Refused { patch };
471 }
472
473 if let Some(position) = account_position {
474 let previous = view.lifetimes.peek(key).cloned();
481 view.lifetimes.put(
482 key.to_string(),
483 EntityLifetimeCheckpoint {
484 account_position: Some(position),
485 source_seq: previous
486 .as_ref()
487 .and_then(|checkpoint| checkpoint.source_seq.clone()),
488 source_sequences: previous
489 .map(|checkpoint| checkpoint.source_sequences)
490 .unwrap_or_default(),
491 deleted: false,
492 },
493 );
494 view.trim_lifetimes();
495 } else {
496 if let (Some(source_seq), Some(source_domain)) = (source_seq, source_domain) {
497 let mut checkpoint = if creation {
498 EntityLifetimeCheckpoint {
499 account_position: None,
500 source_seq: None,
501 source_sequences: BTreeMap::new(),
502 deleted: false,
503 }
504 } else {
505 view.lifetimes
506 .peek(key)
507 .cloned()
508 .unwrap_or(EntityLifetimeCheckpoint {
509 account_position: None,
510 source_seq: None,
511 source_sequences: BTreeMap::new(),
512 deleted: false,
513 })
514 };
515 checkpoint
516 .source_sequences
517 .insert(source_domain, source_seq.to_string());
518 checkpoint.deleted = false;
519 view.lifetimes.put(key.to_string(), checkpoint);
520 view.trim_lifetimes();
521 } else if creation
522 && view.lifetimes.peek(key).is_some_and(|checkpoint| {
523 checkpoint.source_seq.is_none() && checkpoint.source_sequences.is_empty()
524 })
525 {
526 view.lifetimes.pop(key);
529 }
530 }
531
532 let whole = match origin {
533 PatchOrigin::Creation => true,
534 PatchOrigin::Change => view.entities.contains(key),
535 PatchOrigin::Unknown => view.entities.contains(key) || !view.was_evicted(key),
536 };
537 if !whole {
538 return CacheWrite::Refused { patch };
539 }
540
541 if creation {
544 let new_entity = truncate_arrays_if_needed(patch, max_array_length);
545 view.store(key.to_string(), new_entity);
546 return CacheWrite::Created;
547 }
548
549 if let Some(entity) = view.entities.get_mut(key) {
550 deep_merge_with_append(entity, patch, append_paths, max_array_length);
551 return CacheWrite::Merged;
552 }
553 let new_entity = truncate_arrays_if_needed(patch, max_array_length);
554 view.store(key.to_string(), new_entity);
555 CacheWrite::Created
556 }
557
558 pub async fn store_whole(&self, view_id: &str, key: &str, entity: Value) -> bool {
562 self.store_whole_for_lifetime(view_id, key, entity, None)
563 .await
564 }
565
566 pub async fn store_whole_for_lifetime(
569 &self,
570 view_id: &str,
571 key: &str,
572 entity: Value,
573 requested_account_position: Option<AccountPosition>,
574 ) -> bool {
575 self.store_whole_checked(view_id, key, entity, requested_account_position, None)
576 .await
577 }
578
579 pub async fn store_whole_for_ordering(
580 &self,
581 view_id: &str,
582 key: &str,
583 entity: Value,
584 requested_account_position: Option<AccountPosition>,
585 requested_source_seq: Option<&str>,
586 ) -> bool {
587 self.store_whole_for_ordering_in_domain(
588 view_id,
589 key,
590 entity,
591 requested_account_position,
592 requested_source_seq,
593 SlotIndexDomain::Legacy,
594 )
595 .await
596 }
597
598 pub async fn store_whole_for_ordering_in_domain(
599 &self,
600 view_id: &str,
601 key: &str,
602 entity: Value,
603 requested_account_position: Option<AccountPosition>,
604 requested_source_seq: Option<&str>,
605 requested_source_domain: SlotIndexDomain,
606 ) -> bool {
607 self.store_whole_checked(
608 view_id,
609 key,
610 entity,
611 requested_account_position,
612 Some((requested_source_seq, requested_source_domain)),
613 )
614 .await
615 }
616
617 async fn store_whole_checked(
618 &self,
619 view_id: &str,
620 key: &str,
621 entity: Value,
622 requested_account_position: Option<AccountPosition>,
623 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
624 ) -> bool {
625 let mut caches = self.caches.write().await;
626 let view = caches
627 .entry(view_id.to_string())
628 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
629 let entity = truncate_arrays_if_needed(entity, self.config.max_array_length);
630 if let Some(checkpoint) = view.lifetimes.peek(key) {
631 if checkpoint.deleted || checkpoint.account_position != requested_account_position {
632 return false;
633 }
634 if let Some((requested_source_seq, requested_source_domain)) = requested_source {
635 if (checkpoint.account_position.is_none()
636 && checkpoint.source_seq(Some(requested_source_domain)) != requested_source_seq)
637 || view.entities.contains(key)
638 {
639 return false;
640 }
641 } else if checkpoint.account_position.is_some() && view.entities.contains(key) {
642 return false;
643 }
644 }
645 view.store(key.to_string(), entity);
646 true
647 }
648
649 pub async fn account_position(&self, view_id: &str, key: &str) -> Option<AccountPosition> {
650 self.caches
651 .read()
652 .await
653 .get(view_id)
654 .and_then(|view| view.lifetimes.peek(key))
655 .and_then(|checkpoint| checkpoint.account_position)
656 }
657
658 pub async fn accepts_mutation(
659 &self,
660 view_id: &str,
661 key: &str,
662 _entity: &Value,
663 creation: bool,
664 ) -> bool {
665 self.accepts_lifetime_mutation(view_id, key, creation, None)
666 .await
667 }
668
669 pub async fn accepts_lifetime_mutation(
670 &self,
671 view_id: &str,
672 key: &str,
673 creation: bool,
674 account_position: Option<AccountPosition>,
675 ) -> bool {
676 self.accepts_ordered_lifetime_mutation(view_id, key, creation, account_position, None)
677 .await
678 }
679
680 pub async fn accepts_ordered_lifetime_mutation(
681 &self,
682 view_id: &str,
683 key: &str,
684 creation: bool,
685 account_position: Option<AccountPosition>,
686 source_seq: Option<&str>,
687 ) -> bool {
688 self.accepts_ordered_lifetime_mutation_in_domain(
689 view_id,
690 key,
691 creation,
692 account_position,
693 source_seq,
694 SlotIndexDomain::Legacy,
695 )
696 .await
697 }
698
699 pub async fn accepts_ordered_lifetime_mutation_in_domain(
700 &self,
701 view_id: &str,
702 key: &str,
703 creation: bool,
704 account_position: Option<AccountPosition>,
705 source_seq: Option<&str>,
706 source_domain: SlotIndexDomain,
707 ) -> bool {
708 self.caches.read().await.get(view_id).is_none_or(|view| {
709 view.accepts(
710 key,
711 creation,
712 account_position,
713 source_seq,
714 Some(source_domain),
715 )
716 })
717 }
718
719 pub async fn delete(
723 &self,
724 view_id: &str,
725 key: &str,
726 account_position: Option<AccountPosition>,
727 ) -> bool {
728 match account_position {
729 Some(position) => {
730 self.delete_ordered(view_id, key, Some(position), None)
731 .await
732 }
733 None => self.delete_current(view_id, key).await,
734 }
735 }
736
737 pub async fn delete_current(&self, view_id: &str, key: &str) -> bool {
741 let mut caches = self.caches.write().await;
742 let view = caches
743 .entry(view_id.to_string())
744 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
745 let checkpoint = view
746 .lifetimes
747 .peek(key)
748 .cloned()
749 .unwrap_or(EntityLifetimeCheckpoint {
750 account_position: None,
751 source_seq: None,
752 source_sequences: BTreeMap::new(),
753 deleted: false,
754 });
755 if checkpoint.deleted {
756 return true;
757 }
758 view.entities.pop(key);
759 view.forget_evicted(key);
760 view.lifetimes.put(
761 key.to_string(),
762 EntityLifetimeCheckpoint {
763 deleted: true,
764 ..checkpoint
765 },
766 );
767 view.trim_lifetimes();
768 true
769 }
770
771 pub async fn delete_ordered(
774 &self,
775 view_id: &str,
776 key: &str,
777 account_position: Option<AccountPosition>,
778 source_seq: Option<&str>,
779 ) -> bool {
780 self.delete_ordered_in_domain(
781 view_id,
782 key,
783 account_position,
784 source_seq,
785 SlotIndexDomain::Legacy,
786 )
787 .await
788 }
789
790 pub async fn delete_ordered_in_domain(
791 &self,
792 view_id: &str,
793 key: &str,
794 account_position: Option<AccountPosition>,
795 source_seq: Option<&str>,
796 source_domain: SlotIndexDomain,
797 ) -> bool {
798 let mut caches = self.caches.write().await;
799 let view = caches
800 .entry(view_id.to_string())
801 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
802 if let Some(checkpoint) = view.lifetimes.peek(key) {
803 match (checkpoint.account_position, account_position) {
804 (Some(previous), Some(incoming)) if incoming < previous => return false,
805 (Some(previous), Some(incoming)) if incoming == previous => {
806 return checkpoint.deleted;
807 }
808 _ => {}
809 }
810 }
811 if account_position.is_none()
812 && source_seq.is_some()
813 && !view.accepts_source_ordering(key, source_seq, Some(source_domain), true)
814 {
815 return false;
816 }
817 let previous = view.lifetimes.peek(key).cloned();
818 view.entities.pop(key);
819 view.forget_evicted(key);
820 let mut checkpoint = if account_position.is_some() {
821 let previous = previous.unwrap_or(EntityLifetimeCheckpoint {
822 account_position: None,
823 source_seq: None,
824 source_sequences: BTreeMap::new(),
825 deleted: false,
826 });
827 EntityLifetimeCheckpoint {
828 account_position,
829 source_seq: previous.source_seq,
830 source_sequences: previous.source_sequences,
831 deleted: true,
832 }
833 } else {
834 previous.unwrap_or(EntityLifetimeCheckpoint {
835 account_position: None,
836 source_seq: None,
837 source_sequences: BTreeMap::new(),
838 deleted: false,
839 })
840 };
841 if account_position.is_none() {
842 if let Some(source_seq) = source_seq {
843 checkpoint
844 .source_sequences
845 .insert(source_domain, source_seq.to_string());
846 }
847 checkpoint.deleted = true;
848 }
849 view.lifetimes.put(key.to_string(), checkpoint);
850 view.trim_lifetimes();
851 true
852 }
853
854 pub async fn dump_lifetimes(&self) -> EntityLifetimes {
855 self.caches
856 .read()
857 .await
858 .iter()
859 .flat_map(|(id, view)| {
860 view.lifetimes
861 .iter()
862 .map(|(key, checkpoint)| (id.clone(), key.clone(), checkpoint.clone()))
863 })
864 .collect()
865 }
866
867 pub async fn hydrate_lifetimes(&self, lifetimes: EntityLifetimes) {
868 let mut caches = self.caches.write().await;
869 for (id, key, checkpoint) in lifetimes.into_iter().rev() {
870 let view = caches
871 .entry(id)
872 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
873 view.lifetimes.put(key, checkpoint);
874 view.trim_lifetimes();
875 }
876 }
877
878 pub async fn hydrate_legacy_tombstones(&self, tombstones: EntityTombstones) {
882 let mut caches = self.caches.write().await;
883 for (id, key, seq) in tombstones.into_iter().rev() {
884 let view = caches
885 .entry(id)
886 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
887 let deleted = !view.entities.contains(&key);
888 let account_position = seq.as_deref().and_then(account_position_from_seq);
889 view.lifetimes.put(
890 key,
891 EntityLifetimeCheckpoint {
892 account_position,
893 source_seq: None,
894 source_sequences: BTreeMap::new(),
895 deleted,
896 },
897 );
898 view.trim_lifetimes();
899 }
900 }
901
902 pub async fn deletion_is_current(&self, view_id: &str, key: &str) -> bool {
906 let caches = self.caches.read().await;
907 caches.get(view_id).is_none_or(|view| {
908 view.lifetimes
909 .peek(key)
910 .map_or(!view.entities.contains(key), |checkpoint| {
911 checkpoint.deleted
912 })
913 })
914 }
915
916 pub fn merge_patch(&self, base: &mut Value, patch: Value, append_paths: &[String]) {
919 deep_merge_with_append(base, patch, append_paths, self.config.max_array_length);
920 }
921
922 pub async fn remember_evicted(&self, view_id: &str, keys: impl IntoIterator<Item = String>) {
928 let mut caches = self.caches.write().await;
929 let view = caches
930 .entry(view_id.to_string())
931 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
932 for key in keys {
933 if !view.entities.contains(&key) {
934 view.remember_evicted(key);
935 }
936 }
937 }
938
939 pub async fn get_all(&self, view_id: &str) -> Vec<(String, Value)> {
944 let caches = self.caches.read().await;
945
946 caches
947 .get(view_id)
948 .map(|view| {
949 view.entities
950 .iter()
951 .map(|(k, v)| (k.clone(), v.clone()))
952 .collect()
953 })
954 .unwrap_or_default()
955 }
956
957 pub async fn get_after(
963 &self,
964 view_id: &str,
965 cursor: &str,
966 limit: Option<usize>,
967 ) -> Vec<(String, Value)> {
968 let caches = self.caches.read().await;
969
970 if let Some(view) = caches.get(view_id) {
971 let mut results: Vec<(String, Value)> = view
972 .entities
973 .iter()
974 .filter(|(_, entity)| {
975 entity
976 .get("_seq")
977 .and_then(|s| s.as_str())
978 .map(|seq| cmp_seq(seq, cursor) == std::cmp::Ordering::Greater)
979 .unwrap_or(false)
980 })
981 .map(|(k, v)| (k.clone(), v.clone()))
982 .collect();
983
984 results.sort_by(|a, b| {
986 let seq_a = a.1.get("_seq").and_then(|s| s.as_str()).unwrap_or("");
987 let seq_b = b.1.get("_seq").and_then(|s| s.as_str()).unwrap_or("");
988 cmp_seq(seq_a, seq_b)
989 });
990
991 if let Some(limit) = limit {
993 results.truncate(limit);
994 }
995
996 results
997 } else {
998 vec![]
999 }
1000 }
1001
1002 pub async fn get(&self, view_id: &str, key: &str) -> Option<Value> {
1004 let caches = self.caches.read().await;
1005 caches
1006 .get(view_id)
1007 .and_then(|view| view.entities.peek(key).cloned())
1008 }
1009
1010 pub async fn remove(&self, view_id: &str, key: &str) -> Option<Value> {
1015 let mut caches = self.caches.write().await;
1016 caches.get_mut(view_id).and_then(|view| {
1017 view.forget_evicted(key);
1018 view.entities.pop(key)
1019 })
1020 }
1021
1022 pub async fn len(&self, view_id: &str) -> usize {
1024 let caches = self.caches.read().await;
1025 caches
1026 .get(view_id)
1027 .map(|view| view.entities.len())
1028 .unwrap_or(0)
1029 }
1030
1031 pub async fn is_empty(&self, view_id: &str) -> bool {
1033 self.len(view_id).await == 0
1034 }
1035
1036 pub fn snapshot_config(&self) -> SnapshotBatchConfig {
1038 SnapshotBatchConfig {
1039 initial_batch_size: self.config.initial_snapshot_batch_size,
1040 subsequent_batch_size: self.config.subsequent_snapshot_batch_size,
1041 }
1042 }
1043
1044 pub async fn clear(&self, view_id: &str) {
1046 let mut caches = self.caches.write().await;
1047 if let Some(view) = caches.get_mut(view_id) {
1048 view.entities.clear();
1049 view.evicted = None;
1050 }
1051 }
1052
1053 pub async fn clear_all(&self) {
1054 let mut caches = self.caches.write().await;
1055 caches.clear();
1056 }
1057
1058 pub async fn dump(&self) -> Vec<(String, Vec<(String, Value)>)> {
1063 let caches = self.caches.read().await;
1064 caches
1065 .iter()
1066 .map(|(view_id, view)| {
1067 (
1068 view_id.clone(),
1069 view.entities
1070 .iter()
1071 .map(|(key, entity)| (key.clone(), entity.clone()))
1072 .collect(),
1073 )
1074 })
1075 .collect()
1076 }
1077
1078 pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
1083 let mut caches = self.caches.write().await;
1084 caches.clear();
1085 for (view_id, entries) in views {
1086 let view = caches
1087 .entry(view_id)
1088 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1089 for (key, entity) in entries.into_iter().rev() {
1090 view.store(key, entity);
1091 }
1092 }
1093 }
1094
1095 pub async fn stats(&self) -> CacheStats {
1096 let caches = self.caches.read().await;
1097 let mut total_entities = 0;
1098 let mut views = Vec::new();
1099
1100 for (view_id, view) in caches.iter() {
1101 let count = view.entities.len();
1102 total_entities += count;
1103 views.push((view_id.clone(), count));
1104 }
1105
1106 views.sort_by_key(|b| std::cmp::Reverse(b.1));
1107
1108 CacheStats {
1109 view_count: caches.len(),
1110 total_entities,
1111 top_views: views.into_iter().take(5).collect(),
1112 }
1113 }
1114}
1115
1116#[derive(Debug, Clone, PartialEq, Eq)]
1118pub struct CacheStats {
1119 pub view_count: usize,
1120 pub total_entities: usize,
1121 pub top_views: Vec<(String, usize)>,
1122}
1123
1124#[derive(Debug, Clone, Copy)]
1125pub struct SnapshotBatchConfig {
1126 pub initial_batch_size: usize,
1127 pub subsequent_batch_size: usize,
1128}
1129
1130impl Default for EntityCache {
1131 fn default() -> Self {
1132 Self::new()
1133 }
1134}
1135
1136fn deep_merge_with_append(
1137 base: &mut Value,
1138 patch: Value,
1139 append_paths: &[String],
1140 max_array_length: usize,
1141) {
1142 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
1143}
1144
1145fn deep_merge_with_append_inner(
1146 base: &mut Value,
1147 patch: Value,
1148 append_paths: &[String],
1149 current_path: &str,
1150 max_array_length: usize,
1151) {
1152 match (base, patch) {
1153 (Value::Object(base_map), Value::Object(patch_map)) => {
1154 for (key, patch_value) in patch_map {
1155 let child_path = if current_path.is_empty() {
1156 key.clone()
1157 } else {
1158 format!("{}.{}", current_path, key)
1159 };
1160
1161 if let Some(base_value) = base_map.get_mut(&key) {
1162 deep_merge_with_append_inner(
1163 base_value,
1164 patch_value,
1165 append_paths,
1166 &child_path,
1167 max_array_length,
1168 );
1169 } else {
1170 base_map.insert(
1171 key,
1172 truncate_arrays_if_needed(patch_value, max_array_length),
1173 );
1174 }
1175 }
1176 }
1177
1178 (Value::Array(base_arr), Value::Array(patch_arr)) => {
1179 let should_append = append_paths.iter().any(|p| p == current_path);
1180 if should_append {
1181 base_arr.extend(patch_arr);
1182 if base_arr.len() > max_array_length {
1183 let excess = base_arr.len() - max_array_length;
1184 base_arr.drain(0..excess);
1185 }
1186 } else {
1187 *base_arr = patch_arr;
1188 if base_arr.len() > max_array_length {
1189 let excess = base_arr.len() - max_array_length;
1190 base_arr.drain(0..excess);
1191 }
1192 }
1193 }
1194
1195 (base, patch_value) => {
1196 *base = truncate_arrays_if_needed(patch_value, max_array_length);
1197 }
1198 }
1199}
1200
1201fn truncate_arrays_if_needed(value: Value, max_array_length: usize) -> Value {
1203 match value {
1204 Value::Array(mut arr) => {
1205 if arr.len() > max_array_length {
1207 let excess = arr.len() - max_array_length;
1208 arr.drain(0..excess);
1209 }
1210 Value::Array(
1212 arr.into_iter()
1213 .map(|v| truncate_arrays_if_needed(v, max_array_length))
1214 .collect(),
1215 )
1216 }
1217 Value::Object(map) => Value::Object(
1218 map.into_iter()
1219 .map(|(k, v)| (k, truncate_arrays_if_needed(v, max_array_length)))
1220 .collect(),
1221 ),
1222 other => other,
1223 }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::*;
1229 use serde_json::json;
1230
1231 #[tokio::test]
1232 async fn same_slot_instruction_activity_cannot_block_authoritative_deletion() {
1233 let cache = EntityCache::new();
1234 assert_eq!(
1235 cache
1236 .upsert_with_lifetime(
1237 "v",
1238 "a",
1239 json!({"account":true,"_seq":"100:000000000009"}),
1240 &[],
1241 PatchOrigin::Creation,
1242 Some(AccountPosition::new(100, 9)),
1243 )
1244 .await,
1245 CacheWrite::Created
1246 );
1247 assert_eq!(
1248 cache
1249 .upsert_with_append(
1250 "v",
1251 "a",
1252 json!({"instruction":true,"_seq":"100:000000000900"}),
1253 &[],
1254 PatchOrigin::Change
1255 )
1256 .await,
1257 CacheWrite::Merged
1258 );
1259 assert!(
1260 cache
1261 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1262 .await
1263 );
1264 assert!(cache.get("v", "a").await.is_none());
1265 assert!(cache.deletion_is_current("v", "a").await);
1266 }
1267
1268 #[tokio::test]
1269 async fn recreation_has_its_own_ordering_and_replaces_old_fields() {
1270 let cache = EntityCache::with_config(EntityCacheConfig {
1271 max_entities_per_view: 1,
1272 ..Default::default()
1273 });
1274 assert!(
1275 cache
1276 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1277 .await
1278 );
1279 assert_eq!(
1280 cache
1281 .upsert_with_lifetime(
1282 "v",
1283 "a",
1284 json!({"fresh":true, "_seq":"100:000000001002"}),
1285 &[],
1286 PatchOrigin::Creation,
1287 Some(AccountPosition::new(100, 1002)),
1288 )
1289 .await,
1290 CacheWrite::Created
1291 );
1292 assert_eq!(
1293 cache
1294 .upsert_with_append(
1295 "v",
1296 "a",
1297 json!({"instruction":true, "_seq":"100:000000000900"}),
1298 &[],
1299 PatchOrigin::Change
1300 )
1301 .await,
1302 CacheWrite::Merged
1303 );
1304
1305 assert!(matches!(
1308 cache
1309 .upsert_with_lifetime(
1310 "v",
1311 "a",
1312 json!({"old":true, "_seq":"100:000000001001"}),
1313 &[],
1314 PatchOrigin::Change,
1315 Some(AccountPosition::new(100, 1001)),
1316 )
1317 .await,
1318 CacheWrite::Refused { .. }
1319 ));
1320 assert!(
1321 !cache
1322 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1323 .await
1324 );
1325 assert!(
1326 !cache
1327 .store_whole("v", "a", json!({"old":true, "_seq":"100:000000000900"}))
1328 .await
1329 );
1330 let row = cache.get("v", "a").await.unwrap();
1331 assert_eq!(row["fresh"], true);
1332 assert_eq!(row["instruction"], true);
1333 assert!(row.get("old").is_none());
1334
1335 cache
1336 .upsert_with_append(
1337 "v",
1338 "other",
1339 json!({"other":true}),
1340 &[],
1341 PatchOrigin::Creation,
1342 )
1343 .await;
1344 assert!(cache.get("v", "a").await.is_none());
1345 assert!(matches!(
1346 cache
1347 .upsert_with_lifetime(
1348 "v",
1349 "a",
1350 json!({"accountUpdate":true, "_seq":"100:000000001003"}),
1351 &[],
1352 PatchOrigin::Change,
1353 Some(AccountPosition::new(100, 1003)),
1354 )
1355 .await,
1356 CacheWrite::Refused { .. }
1357 ));
1358 assert!(
1359 !cache
1360 .store_whole_for_lifetime(
1361 "v",
1362 "a",
1363 row.clone(),
1364 Some(AccountPosition::new(100, 1002)),
1365 )
1366 .await
1367 );
1368 let mut current = row;
1369 current["accountUpdate"] = Value::Bool(true);
1370 current["_seq"] = Value::String("100:000000001003".into());
1371 assert!(
1372 cache
1373 .store_whole_for_lifetime("v", "a", current, Some(AccountPosition::new(100, 1003)),)
1374 .await
1375 );
1376
1377 let restored = EntityCache::new();
1380 restored.hydrate(cache.dump().await).await;
1381 restored
1382 .hydrate_lifetimes(cache.dump_lifetimes().await)
1383 .await;
1384 assert_eq!(
1385 restored
1386 .upsert_with_append(
1387 "v",
1388 "a",
1389 json!({"afterRestore":true, "_seq":"100:000000000901"}),
1390 &[],
1391 PatchOrigin::Change,
1392 )
1393 .await,
1394 CacheWrite::Merged
1395 );
1396 assert!(matches!(
1397 restored
1398 .upsert_with_lifetime(
1399 "v",
1400 "a",
1401 json!({"old":true, "_seq":"100:000000001001"}),
1402 &[],
1403 PatchOrigin::Change,
1404 Some(AccountPosition::new(100, 1001)),
1405 )
1406 .await,
1407 CacheWrite::Refused { .. }
1408 ));
1409 }
1410
1411 #[tokio::test]
1412 async fn recreation_retains_each_source_cursor_as_an_old_lifetime_barrier() {
1413 let cache = EntityCache::new();
1414 assert_eq!(
1415 cache
1416 .upsert_with_lifetime(
1417 "v",
1418 "a",
1419 json!({"lifetime":"old"}),
1420 &[],
1421 PatchOrigin::Creation,
1422 Some(AccountPosition::new(100, 9)),
1423 )
1424 .await,
1425 CacheWrite::Created
1426 );
1427 for (domain, seq, field) in [
1428 (
1429 SlotIndexDomain::Instruction,
1430 "100:000000000800",
1431 "oldInstruction",
1432 ),
1433 (SlotIndexDomain::Resolver, "100:900000000000", "oldResolver"),
1434 ] {
1435 let mut patch = json!({"_seq":seq});
1436 patch[field] = Value::Bool(true);
1437 assert_eq!(
1438 cache
1439 .upsert_with_ordering(
1440 "v",
1441 "a",
1442 patch,
1443 &[],
1444 PatchOrigin::Change,
1445 LifetimeOrdering {
1446 account_position: None,
1447 source_seq: Some(seq),
1448 source_domain: Some(domain),
1449 },
1450 )
1451 .await,
1452 CacheWrite::Merged
1453 );
1454 }
1455
1456 assert!(
1457 cache
1458 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1459 .await
1460 );
1461 assert_eq!(
1462 cache
1463 .upsert_with_lifetime(
1464 "v",
1465 "a",
1466 json!({"lifetime":"new"}),
1467 &[],
1468 PatchOrigin::Creation,
1469 Some(AccountPosition::new(100, 11)),
1470 )
1471 .await,
1472 CacheWrite::Created
1473 );
1474
1475 for (domain, seq, field) in [
1478 (
1479 SlotIndexDomain::Instruction,
1480 "100:000000000799",
1481 "staleInstruction",
1482 ),
1483 (
1484 SlotIndexDomain::Resolver,
1485 "100:899999999999",
1486 "staleResolver",
1487 ),
1488 ] {
1489 let mut patch = json!({"_seq":seq});
1490 patch[field] = Value::Bool(true);
1491 assert!(matches!(
1492 cache
1493 .upsert_with_ordering(
1494 "v",
1495 "a",
1496 patch,
1497 &[],
1498 PatchOrigin::Change,
1499 LifetimeOrdering {
1500 account_position: None,
1501 source_seq: Some(seq),
1502 source_domain: Some(domain),
1503 },
1504 )
1505 .await,
1506 CacheWrite::Refused { .. }
1507 ));
1508 }
1509
1510 assert_eq!(
1513 cache
1514 .upsert_with_ordering(
1515 "v",
1516 "a",
1517 json!({"newInstruction":true, "_seq":"100:000000000900"}),
1518 &[],
1519 PatchOrigin::Change,
1520 LifetimeOrdering {
1521 account_position: None,
1522 source_seq: Some("100:000000000900"),
1523 source_domain: Some(SlotIndexDomain::Instruction),
1524 },
1525 )
1526 .await,
1527 CacheWrite::Merged
1528 );
1529 let row = cache.get("v", "a").await.unwrap();
1530 assert_eq!(row["lifetime"], "new");
1531 assert_eq!(row["newInstruction"], true);
1532 assert!(row.get("oldInstruction").is_none());
1533 assert!(row.get("oldResolver").is_none());
1534 assert!(row.get("staleInstruction").is_none());
1535 assert!(row.get("staleResolver").is_none());
1536
1537 let restored = EntityCache::new();
1538 restored.hydrate(cache.dump().await).await;
1539 restored
1540 .hydrate_lifetimes(cache.dump_lifetimes().await)
1541 .await;
1542 assert!(matches!(
1543 restored
1544 .upsert_with_ordering(
1545 "v",
1546 "a",
1547 json!({"staleAfterRestore":true}),
1548 &[],
1549 PatchOrigin::Change,
1550 LifetimeOrdering {
1551 account_position: None,
1552 source_seq: Some("100:000000000799"),
1553 source_domain: Some(SlotIndexDomain::Instruction),
1554 },
1555 )
1556 .await,
1557 CacheWrite::Refused { .. }
1558 ));
1559 }
1560
1561 #[tokio::test]
1562 async fn markerless_sources_keep_seq_ordering_across_recreation_and_restore() {
1563 let cache = EntityCache::new();
1564 assert_eq!(
1565 cache
1566 .upsert_with_append(
1567 "v",
1568 "a",
1569 json!({"old":true,"_seq":"100:000000000010"}),
1570 &[],
1571 PatchOrigin::Creation,
1572 )
1573 .await,
1574 CacheWrite::Created
1575 );
1576 assert!(
1577 !cache
1578 .delete_ordered("v", "a", None, Some("100:000000000009"))
1579 .await
1580 );
1581 assert!(cache.get("v", "a").await.is_some());
1582 assert!(
1583 cache
1584 .delete_ordered("v", "a", None, Some("100:000000000011"))
1585 .await
1586 );
1587 assert_eq!(
1588 cache
1589 .upsert_with_append(
1590 "v",
1591 "a",
1592 json!({"fresh":true,"_seq":"100:000000000012"}),
1593 &[],
1594 PatchOrigin::Creation,
1595 )
1596 .await,
1597 CacheWrite::Created
1598 );
1599 assert!(matches!(
1600 cache
1601 .upsert_with_append(
1602 "v",
1603 "a",
1604 json!({"stale":true,"_seq":"100:000000000010"}),
1605 &[],
1606 PatchOrigin::Creation,
1607 )
1608 .await,
1609 CacheWrite::Refused { .. }
1610 ));
1611 assert!(
1612 !cache
1613 .delete_ordered("v", "a", None, Some("100:000000000011"))
1614 .await
1615 );
1616
1617 let restored = EntityCache::new();
1618 restored.hydrate(cache.dump().await).await;
1619 restored
1620 .hydrate_lifetimes(cache.dump_lifetimes().await)
1621 .await;
1622 assert!(
1623 !restored
1624 .delete_ordered("v", "a", None, Some("100:000000000011"))
1625 .await
1626 );
1627 let row = restored.get("v", "a").await.unwrap();
1628 assert_eq!(row["fresh"], true);
1629 assert!(row.get("old").is_none());
1630 assert!(row.get("stale").is_none());
1631
1632 let resync_cache = EntityCache::new();
1635 resync_cache
1636 .hydrate_lifetimes(cache.dump_lifetimes().await)
1637 .await;
1638 assert!(
1639 !resync_cache
1640 .store_whole_for_ordering(
1641 "v",
1642 "a",
1643 json!({"stale_resend":true}),
1644 None,
1645 Some("100:000000000010"),
1646 )
1647 .await
1648 );
1649 assert!(
1650 resync_cache
1651 .store_whole_for_ordering(
1652 "v",
1653 "a",
1654 json!({"fresh_resend":true}),
1655 None,
1656 Some("100:000000000012"),
1657 )
1658 .await
1659 );
1660 }
1661
1662 #[tokio::test]
1663 async fn older_source_patch_cannot_replace_newer_row_or_move_its_cursor_back() {
1664 let cache = EntityCache::new();
1665 assert_eq!(
1666 cache
1667 .upsert_with_append(
1668 "v",
1669 "a",
1670 json!({"value":"created","_seq":"100:000000000010"}),
1671 &[],
1672 PatchOrigin::Creation,
1673 )
1674 .await,
1675 CacheWrite::Created
1676 );
1677 assert_eq!(
1678 cache
1679 .upsert_with_ordering(
1680 "v",
1681 "a",
1682 json!({"value":"newer","_seq":"100:000000000012"}),
1683 &[],
1684 PatchOrigin::Change,
1685 LifetimeOrdering {
1686 account_position: None,
1687 source_seq: Some("100:000000000012"),
1688 source_domain: Some(SlotIndexDomain::Legacy),
1689 },
1690 )
1691 .await,
1692 CacheWrite::Merged
1693 );
1694 assert!(matches!(
1695 cache
1696 .upsert_with_ordering(
1697 "v",
1698 "a",
1699 json!({"value":"older","stale":true,"_seq":"100:000000000011"}),
1700 &[],
1701 PatchOrigin::Change,
1702 LifetimeOrdering {
1703 account_position: None,
1704 source_seq: Some("100:000000000011"),
1705 source_domain: Some(SlotIndexDomain::Legacy),
1706 },
1707 )
1708 .await,
1709 CacheWrite::Refused { .. }
1710 ));
1711 assert!(
1712 !cache
1713 .delete_ordered("v", "a", None, Some("100:000000000011"))
1714 .await
1715 );
1716 let row = cache.get("v", "a").await.unwrap();
1717 assert_eq!(row["value"], "newer");
1718 assert_eq!(row["_seq"], "100:000000000012");
1719 assert!(row.get("stale").is_none());
1720 }
1721
1722 #[tokio::test]
1723 async fn independent_source_recency_domains_survive_snapshot_restore() {
1724 let cache = EntityCache::new();
1725 cache
1726 .upsert_with_lifetime(
1727 "v",
1728 "a",
1729 json!({"value":"created"}),
1730 &[],
1731 PatchOrigin::Creation,
1732 Some(AccountPosition::new(100, 9)),
1733 )
1734 .await;
1735 for (domain, seq, value) in [
1736 (SlotIndexDomain::Resolver, "100:900000000000", "resolver"),
1737 (
1738 SlotIndexDomain::Instruction,
1739 "100:000000000900",
1740 "instruction",
1741 ),
1742 ] {
1743 assert_eq!(
1744 cache
1745 .upsert_with_ordering(
1746 "v",
1747 "a",
1748 json!({"value":value, "_seq":seq}),
1749 &[],
1750 PatchOrigin::Change,
1751 LifetimeOrdering {
1752 account_position: None,
1753 source_seq: Some(seq),
1754 source_domain: Some(domain),
1755 },
1756 )
1757 .await,
1758 CacheWrite::Merged
1759 );
1760 }
1761
1762 let restored = EntityCache::new();
1763 restored.hydrate(cache.dump().await).await;
1764 restored
1765 .hydrate_lifetimes(cache.dump_lifetimes().await)
1766 .await;
1767
1768 for (domain, seq) in [
1769 (SlotIndexDomain::Instruction, "100:000000000899"),
1770 (SlotIndexDomain::Resolver, "100:899999999999"),
1771 ] {
1772 assert!(matches!(
1773 restored
1774 .upsert_with_ordering(
1775 "v",
1776 "a",
1777 json!({"stale":true, "_seq":seq}),
1778 &[],
1779 PatchOrigin::Change,
1780 LifetimeOrdering {
1781 account_position: None,
1782 source_seq: Some(seq),
1783 source_domain: Some(domain),
1784 },
1785 )
1786 .await,
1787 CacheWrite::Refused { .. }
1788 ));
1789 }
1790 assert_eq!(
1791 restored.get("v", "a").await.unwrap()["value"],
1792 "instruction"
1793 );
1794 }
1795
1796 #[tokio::test]
1797 async fn explicit_current_delete_accepts_a_source_without_position() {
1798 let cache = EntityCache::new();
1799 cache
1800 .upsert_with_append(
1801 "v",
1802 "a",
1803 json!({"value":true,"_seq":"100:000000000010"}),
1804 &[],
1805 PatchOrigin::Creation,
1806 )
1807 .await;
1808
1809 assert!(cache.delete("v", "a", None).await);
1810 assert!(cache.get("v", "a").await.is_none());
1811 assert!(cache.deletion_is_current("v", "a").await);
1812 }
1813
1814 #[tokio::test]
1815 async fn evicted_live_row_does_not_make_an_earlier_delete_current() {
1816 let cache = EntityCache::with_config(EntityCacheConfig {
1817 max_entities_per_view: 1,
1818 ..Default::default()
1819 });
1820 cache
1821 .upsert_with_lifetime(
1822 "v",
1823 "a",
1824 json!({"value":true,"_seq":"100:000000000010"}),
1825 &[],
1826 PatchOrigin::Creation,
1827 Some(AccountPosition::new(100, 10)),
1828 )
1829 .await;
1830 cache
1831 .upsert_with_append("v", "b", json!({"value":true}), &[], PatchOrigin::Creation)
1832 .await;
1833
1834 assert!(cache.get("v", "a").await.is_none());
1835 assert!(!cache.deletion_is_current("v", "a").await);
1836 assert!(cache.delete_current("v", "a").await);
1837 assert!(cache.deletion_is_current("v", "a").await);
1838 }
1839
1840 #[tokio::test]
1841 async fn test_basic_upsert_and_get() {
1842 let cache = EntityCache::new();
1843
1844 cache
1845 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
1846 .await;
1847
1848 let entity = cache.get("tokens/list", "abc123").await;
1849 assert!(entity.is_some());
1850 assert_eq!(entity.unwrap()["name"], "Test Token");
1851 }
1852
1853 #[tokio::test]
1854 async fn test_deep_merge_objects() {
1855 let cache = EntityCache::new();
1856
1857 cache
1858 .upsert(
1859 "tokens/list",
1860 "abc123",
1861 json!({
1862 "id": "abc123",
1863 "metrics": {"volume": 100}
1864 }),
1865 )
1866 .await;
1867
1868 cache
1869 .upsert(
1870 "tokens/list",
1871 "abc123",
1872 json!({
1873 "metrics": {"trades": 50}
1874 }),
1875 )
1876 .await;
1877
1878 let entity = cache.get("tokens/list", "abc123").await.unwrap();
1879 assert_eq!(entity["id"], "abc123");
1880 assert_eq!(entity["metrics"]["volume"], 100);
1881 assert_eq!(entity["metrics"]["trades"], 50);
1882 }
1883
1884 #[tokio::test]
1885 async fn test_array_append() {
1886 let cache = EntityCache::new();
1887
1888 cache
1889 .upsert(
1890 "tokens/list",
1891 "abc123",
1892 json!({
1893 "events": [{"type": "buy", "amount": 100}]
1894 }),
1895 )
1896 .await;
1897
1898 cache
1899 .upsert_with_append(
1900 "tokens/list",
1901 "abc123",
1902 json!({
1903 "events": [{"type": "sell", "amount": 50}]
1904 }),
1905 &["events".to_string()],
1906 PatchOrigin::Unknown,
1907 )
1908 .await;
1909
1910 let entity = cache.get("tokens/list", "abc123").await.unwrap();
1911 let events = entity["events"].as_array().unwrap();
1912 assert_eq!(events.len(), 2);
1913 assert_eq!(events[0]["type"], "buy");
1914 assert_eq!(events[1]["type"], "sell");
1915 }
1916
1917 #[tokio::test]
1918 async fn test_array_lru_eviction() {
1919 let config = EntityCacheConfig {
1920 max_entities_per_view: 1000,
1921 max_array_length: 3,
1922 ..Default::default()
1923 };
1924 let cache = EntityCache::with_config(config);
1925
1926 cache
1927 .upsert(
1928 "tokens/list",
1929 "abc123",
1930 json!({
1931 "events": [
1932 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
1933 ]
1934 }),
1935 )
1936 .await;
1937
1938 let entity = cache.get("tokens/list", "abc123").await.unwrap();
1939 let events = entity["events"].as_array().unwrap();
1940
1941 assert_eq!(events.len(), 3);
1942 assert_eq!(events[0]["id"], 3);
1943 assert_eq!(events[1]["id"], 4);
1944 assert_eq!(events[2]["id"], 5);
1945 }
1946
1947 #[tokio::test]
1948 async fn test_array_append_with_lru() {
1949 let config = EntityCacheConfig {
1950 max_entities_per_view: 1000,
1951 max_array_length: 3,
1952 ..Default::default()
1953 };
1954 let cache = EntityCache::with_config(config);
1955
1956 cache
1957 .upsert(
1958 "tokens/list",
1959 "abc123",
1960 json!({
1961 "events": [{"id": 1}, {"id": 2}]
1962 }),
1963 )
1964 .await;
1965
1966 cache
1967 .upsert_with_append(
1968 "tokens/list",
1969 "abc123",
1970 json!({
1971 "events": [{"id": 3}, {"id": 4}]
1972 }),
1973 &["events".to_string()],
1974 PatchOrigin::Unknown,
1975 )
1976 .await;
1977
1978 let entity = cache.get("tokens/list", "abc123").await.unwrap();
1979 let events = entity["events"].as_array().unwrap();
1980
1981 assert_eq!(events.len(), 3);
1983 assert_eq!(events[0]["id"], 2);
1984 assert_eq!(events[1]["id"], 3);
1985 assert_eq!(events[2]["id"], 4);
1986 }
1987
1988 #[tokio::test]
1989 async fn test_entity_lru_eviction() {
1990 let config = EntityCacheConfig {
1991 max_entities_per_view: 2,
1992 max_array_length: 100,
1993 ..Default::default()
1994 };
1995 let cache = EntityCache::with_config(config);
1996
1997 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
1998 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
1999 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
2000
2001 assert!(cache.get("tokens/list", "key1").await.is_none());
2002 assert!(cache.get("tokens/list", "key2").await.is_some());
2003 assert!(cache.get("tokens/list", "key3").await.is_some());
2004 }
2005
2006 #[tokio::test]
2007 async fn a_patch_for_an_evicted_key_is_refused_not_stored() {
2008 let cache = EntityCache::with_config(EntityCacheConfig {
2009 max_entities_per_view: 2,
2010 ..Default::default()
2011 });
2012 for id in 1..=3 {
2013 assert_eq!(
2014 cache
2015 .upsert(
2016 "tokens/list",
2017 &format!("key{id}"),
2018 json!({"id": id, "n": 0})
2019 )
2020 .await,
2021 CacheWrite::Created
2022 );
2023 }
2024 assert_eq!(
2026 cache.upsert("tokens/list", "key1", json!({"n": 5})).await,
2027 CacheWrite::Refused {
2028 patch: json!({"n": 5})
2029 }
2030 );
2031 assert!(cache.get("tokens/list", "key1").await.is_none());
2032 assert_eq!(
2033 cache.upsert("tokens/list", "key3", json!({"n": 1})).await,
2034 CacheWrite::Merged
2035 );
2036
2037 cache.remove("tokens/list", "key1").await;
2039 assert_eq!(
2040 cache
2041 .upsert("tokens/list", "key1", json!({"id": 1, "n": 6}))
2042 .await,
2043 CacheWrite::Created
2044 );
2045 }
2046
2047 #[tokio::test]
2048 async fn a_whole_entity_ends_an_eviction() {
2049 let cache = EntityCache::with_config(EntityCacheConfig {
2050 max_entities_per_view: 1,
2051 ..Default::default()
2052 });
2053 cache.upsert("v", "a", json!({"id": "a", "n": 0})).await;
2054 cache.upsert("v", "b", json!({"id": "b", "n": 0})).await;
2055 assert!(matches!(
2056 cache.upsert("v", "a", json!({"n": 1})).await,
2057 CacheWrite::Refused { .. }
2058 ));
2059 cache
2060 .store_whole("v", "a", json!({"id": "a", "n": 1}))
2061 .await;
2062 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2063 assert_eq!(cache.len("v").await, 1, "still bounded: b made room");
2064 assert_eq!(
2065 cache.upsert("v", "a", json!({"n": 2})).await,
2066 CacheWrite::Merged
2067 );
2068 }
2069
2070 #[tokio::test]
2071 async fn the_evicted_key_memory_is_bounded() {
2072 let cache = EntityCache::with_config(EntityCacheConfig {
2073 max_entities_per_view: 1,
2074 ..Default::default()
2075 });
2076 let remembered = EVICTED_KEYS_PER_CACHED_ENTITY;
2077 for key in 0..=remembered + 1 {
2078 cache
2079 .upsert("v", &key.to_string(), json!({"id": key}))
2080 .await;
2081 }
2082 assert_eq!(
2085 cache.upsert("v", "0", json!({"id": 0})).await,
2086 CacheWrite::Created
2087 );
2088 assert!(matches!(
2089 cache
2090 .upsert("v", &remembered.to_string(), json!({"n": 1}))
2091 .await,
2092 CacheWrite::Refused { .. }
2093 ));
2094 }
2095
2096 async fn write(
2097 cache: &EntityCache,
2098 key: &str,
2099 patch: Value,
2100 origin: PatchOrigin,
2101 ) -> CacheWrite {
2102 cache.upsert_with_append("v", key, patch, &[], origin).await
2103 }
2104
2105 #[tokio::test]
2108 async fn an_unmarked_patch_for_a_key_not_held_is_refused_however_old() {
2109 let cache = EntityCache::with_config(EntityCacheConfig {
2110 max_entities_per_view: 1,
2111 ..Default::default()
2112 });
2113 let evictions = EVICTED_KEYS_PER_CACHED_ENTITY * 4;
2114 for key in 0..=evictions {
2115 assert_eq!(
2116 write(
2117 &cache,
2118 &key.to_string(),
2119 json!({"id": key}),
2120 PatchOrigin::Creation
2121 )
2122 .await,
2123 CacheWrite::Created,
2124 "a creation is stored as soon as it arrives"
2125 );
2126 }
2127 assert_eq!(
2129 write(&cache, "0", json!({"n": 1}), PatchOrigin::Change).await,
2130 CacheWrite::Refused {
2131 patch: json!({"n": 1})
2132 }
2133 );
2134 assert!(
2135 matches!(
2136 write(&cache, "never", json!({"n": 1}), PatchOrigin::Change).await,
2137 CacheWrite::Refused { .. }
2138 ),
2139 "nor does it matter whether the view ever held the key"
2140 );
2141 assert_eq!(cache.get("v", "0").await, None);
2142 assert_eq!(
2143 write(
2144 &cache,
2145 &evictions.to_string(),
2146 json!({"n": 1}),
2147 PatchOrigin::Change
2148 )
2149 .await,
2150 CacheWrite::Merged
2151 );
2152 assert_eq!(
2154 write(&cache, "0", json!({"id": 0, "n": 2}), PatchOrigin::Creation).await,
2155 CacheWrite::Created
2156 );
2157 assert_eq!(cache.get("v", "0").await, Some(json!({"id": 0, "n": 2})));
2158 }
2159
2160 #[tokio::test]
2163 async fn a_creation_for_a_key_held_replaces() {
2164 let cache = EntityCache::new();
2165 write(
2166 &cache,
2167 "a",
2168 json!({"id": "a", "old": 1}),
2169 PatchOrigin::Creation,
2170 )
2171 .await;
2172 assert_eq!(
2173 write(
2174 &cache,
2175 "a",
2176 json!({"id": "a", "n": 1}),
2177 PatchOrigin::Creation
2178 )
2179 .await,
2180 CacheWrite::Created
2181 );
2182 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2183 }
2184
2185 async fn remembers_evictions(cache: &EntityCache, view_id: &str) -> bool {
2186 cache
2187 .caches
2188 .read()
2189 .await
2190 .get(view_id)
2191 .is_some_and(|view| view.evicted.is_some())
2192 }
2193
2194 #[tokio::test]
2198 async fn a_view_fed_marked_creations_keeps_no_eviction_memory() {
2199 let cache = EntityCache::with_config(EntityCacheConfig {
2200 max_entities_per_view: 1,
2201 ..Default::default()
2202 });
2203 cache.remember_evicted("v", ["restored".to_string()]).await;
2204 assert!(remembers_evictions(&cache, "v").await);
2205
2206 write(&cache, "a", json!({"id": "a"}), PatchOrigin::Creation).await;
2207 assert!(!remembers_evictions(&cache, "v").await);
2208 write(&cache, "b", json!({"id": "b"}), PatchOrigin::Creation).await;
2209 assert!(!remembers_evictions(&cache, "v").await, "a was evicted");
2210 assert!(matches!(
2211 write(&cache, "restored", json!({"n": 1}), PatchOrigin::Change).await,
2212 CacheWrite::Refused { .. }
2213 ));
2214 assert!(matches!(
2215 write(&cache, "a", json!({"n": 1}), PatchOrigin::Change).await,
2216 CacheWrite::Refused { .. }
2217 ));
2218 }
2219
2220 #[tokio::test]
2221 async fn remembered_keys_are_refused_unless_held() {
2222 let cache = EntityCache::new();
2223 cache.upsert("tokens/list", "held", json!({"id": 1})).await;
2224 cache
2225 .remember_evicted("tokens/list", ["held".to_string(), "gone".to_string()])
2226 .await;
2227 assert_eq!(
2228 cache.upsert("tokens/list", "held", json!({"n": 1})).await,
2229 CacheWrite::Merged
2230 );
2231 assert!(matches!(
2232 cache.upsert("tokens/list", "gone", json!({"n": 1})).await,
2233 CacheWrite::Refused { .. }
2234 ));
2235 assert_eq!(
2237 cache.upsert("tokens/state", "gone", json!({"id": 2})).await,
2238 CacheWrite::Created
2239 );
2240 }
2241
2242 #[tokio::test]
2243 async fn test_get_all() {
2244 let cache = EntityCache::new();
2245
2246 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2247 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2248
2249 let all = cache.get_all("tokens/list").await;
2250 assert_eq!(all.len(), 2);
2251 }
2252
2253 #[tokio::test]
2254 async fn remove_is_scoped_to_one_entity() {
2255 let cache = EntityCache::new();
2256 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
2257 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
2258
2259 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
2260 assert!(cache.get("tokens/list", "one").await.is_none());
2261 assert!(cache.get("tokens/list", "two").await.is_some());
2262 }
2263
2264 #[tokio::test]
2265 async fn test_separate_views() {
2266 let cache = EntityCache::new();
2267
2268 cache
2269 .upsert("tokens/list", "key1", json!({"type": "token"}))
2270 .await;
2271 cache
2272 .upsert("games/list", "key1", json!({"type": "game"}))
2273 .await;
2274
2275 let token = cache.get("tokens/list", "key1").await.unwrap();
2276 let game = cache.get("games/list", "key1").await.unwrap();
2277
2278 assert_eq!(token["type"], "token");
2279 assert_eq!(game["type"], "game");
2280 }
2281
2282 #[test]
2283 fn test_deep_merge_with_append() {
2284 let mut base = json!({
2285 "a": 1,
2286 "b": {"c": 2},
2287 "arr": [1, 2]
2288 });
2289
2290 let patch = json!({
2291 "b": {"d": 3},
2292 "arr": [3],
2293 "e": 4
2294 });
2295
2296 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
2297
2298 assert_eq!(base["a"], 1);
2299 assert_eq!(base["b"]["c"], 2);
2300 assert_eq!(base["b"]["d"], 3);
2301 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
2302 assert_eq!(base["e"], 4);
2303 }
2304
2305 #[test]
2306 fn test_deep_merge_replace_array() {
2307 let mut base = json!({
2308 "arr": [1, 2, 3]
2309 });
2310
2311 let patch = json!({
2312 "arr": [4, 5]
2313 });
2314
2315 deep_merge_with_append(&mut base, patch, &[], 100);
2316
2317 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
2318 assert_eq!(base["arr"][0], 4);
2319 assert_eq!(base["arr"][1], 5);
2320 }
2321
2322 #[test]
2323 fn test_deep_merge_nested_append() {
2324 let mut base = json!({
2325 "stats": {"events": [1, 2]}
2326 });
2327
2328 let patch = json!({
2329 "stats": {"events": [3]}
2330 });
2331
2332 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
2333
2334 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
2335 }
2336
2337 #[test]
2338 fn test_snapshot_config_defaults() {
2339 let cache = EntityCache::new();
2340 let config = cache.snapshot_config();
2341
2342 assert_eq!(config.initial_batch_size, 50);
2343 assert_eq!(config.subsequent_batch_size, 100);
2344 }
2345
2346 #[test]
2347 fn test_snapshot_config_custom() {
2348 let config = EntityCacheConfig {
2349 initial_snapshot_batch_size: 25,
2350 subsequent_snapshot_batch_size: 75,
2351 ..Default::default()
2352 };
2353 let cache = EntityCache::with_config(config);
2354 let snapshot_config = cache.snapshot_config();
2355
2356 assert_eq!(snapshot_config.initial_batch_size, 25);
2357 assert_eq!(snapshot_config.subsequent_batch_size, 75);
2358 }
2359
2360 #[tokio::test]
2361 async fn test_get_after() {
2362 let cache = EntityCache::new();
2363
2364 cache
2366 .upsert(
2367 "tokens/list",
2368 "key1",
2369 json!({"id": 1, "_seq": "100:000000000001"}),
2370 )
2371 .await;
2372 cache
2373 .upsert(
2374 "tokens/list",
2375 "key2",
2376 json!({"id": 2, "_seq": "100:000000000002"}),
2377 )
2378 .await;
2379 cache
2380 .upsert(
2381 "tokens/list",
2382 "key3",
2383 json!({"id": 3, "_seq": "100:000000000003"}),
2384 )
2385 .await;
2386 cache
2387 .upsert(
2388 "tokens/list",
2389 "key4",
2390 json!({"id": 4, "_seq": "101:000000000001"}),
2391 )
2392 .await;
2393
2394 let after = cache
2396 .get_after("tokens/list", "100:000000000002", None)
2397 .await;
2398
2399 assert_eq!(after.len(), 2);
2401 assert_eq!(after[0].0, "key3");
2402 assert_eq!(after[1].0, "key4");
2403 }
2404
2405 #[tokio::test]
2406 async fn test_get_after_with_limit() {
2407 let cache = EntityCache::new();
2408
2409 cache
2411 .upsert(
2412 "tokens/list",
2413 "key1",
2414 json!({"id": 1, "_seq": "100:000000000001"}),
2415 )
2416 .await;
2417 cache
2418 .upsert(
2419 "tokens/list",
2420 "key2",
2421 json!({"id": 2, "_seq": "100:000000000002"}),
2422 )
2423 .await;
2424 cache
2425 .upsert(
2426 "tokens/list",
2427 "key3",
2428 json!({"id": 3, "_seq": "100:000000000003"}),
2429 )
2430 .await;
2431
2432 let after = cache
2434 .get_after("tokens/list", "100:000000000000", Some(2))
2435 .await;
2436
2437 assert_eq!(after.len(), 2);
2439 assert_eq!(after[0].0, "key1");
2440 assert_eq!(after[1].0, "key2");
2441 }
2442
2443 #[tokio::test]
2444 async fn test_get_after_empty_result() {
2445 let cache = EntityCache::new();
2446
2447 cache
2448 .upsert(
2449 "tokens/list",
2450 "key1",
2451 json!({"id": 1, "_seq": "100:000000000001"}),
2452 )
2453 .await;
2454
2455 let after = cache
2457 .get_after("tokens/list", "999:000000000000", None)
2458 .await;
2459
2460 assert!(after.is_empty());
2461 }
2462
2463 #[tokio::test]
2464 async fn test_get_after_missing_seq() {
2465 let cache = EntityCache::new();
2466
2467 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2469
2470 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
2472
2473 assert!(after.is_empty());
2474 }
2475}