1use crate::mutation_batch::SlotIndexDomain;
8use crate::shared_entity::{
9 split_version, with_version, EntityFields, Fields, 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(
622 &self,
623 key: &str,
624 patch: Value,
625 views: &[ViewVersion<'_>],
626 append_paths: &[String],
627 origin: PatchOrigin,
628 ordering: LifetimeOrdering<'_>,
629 ) -> Vec<CacheWrite> {
630 let (mut patch, stray_version) = split_version(patch);
633 let version_of = |index: usize| {
634 views[index]
635 .version
636 .clone()
637 .or_else(|| stray_version.clone())
638 };
639 let max_array_length = self.config.max_array_length;
640 let mut caches = self.caches.write().await;
641
642 let mut admissions: Vec<Admission> = views
643 .iter()
644 .map(|view| {
645 caches
646 .entry(view.view_id.to_string())
647 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view))
648 .admit(key, origin, ordering)
649 })
650 .collect();
651
652 let mut writes = vec![CacheWrite::Merged; views.len()];
653 for (index, admission) in admissions.iter().enumerate() {
654 if *admission == Admission::Refuse {
655 writes[index] = CacheWrite::Refused {
656 patch: with_version(patch.clone(), version_of(index)),
657 };
658 }
659 }
660
661 let placeholder = Arc::new(Fields::Other(Value::Null));
664 let mut groups: Vec<(Arc<Fields>, Vec<usize>)> = Vec::new();
665 for (index, admission) in admissions.iter_mut().enumerate() {
666 if *admission != Admission::Merge {
667 continue;
668 }
669 let held = caches
670 .get_mut(views[index].view_id)
671 .and_then(|view| view.entities.get_mut(key));
672 let Some(entity) = held else {
673 *admission = Admission::Create;
676 continue;
677 };
678 let fields = std::mem::replace(entity.fields_mut(), placeholder.clone());
679 match groups
680 .iter_mut()
681 .find(|(shared, _)| Arc::ptr_eq(shared, &fields))
682 {
683 Some((_, members)) => members.push(index),
684 None => groups.push((fields, vec![index])),
685 }
686 }
687
688 if admissions.contains(&Admission::Create) {
689 let source = if groups.is_empty() {
690 std::mem::take(&mut patch)
691 } else {
692 patch.clone()
693 };
694 let created = Arc::new(Fields::from_value(truncate_arrays_if_needed(
695 source,
696 max_array_length,
697 )));
698 for (index, admission) in admissions.iter().enumerate() {
699 if *admission != Admission::Create {
700 continue;
701 }
702 let version = version_of(index)
703 .filter(|_| created.is_object())
704 .map(|version| truncate_arrays_if_needed(version, max_array_length));
705 if let Some(view) = caches.get_mut(views[index].view_id) {
706 view.store(
707 key.to_string(),
708 SharedEntity::from_parts(created.clone(), version),
709 );
710 }
711 writes[index] = CacheWrite::Created;
712 }
713 }
714
715 let last_group = groups.len().saturating_sub(1);
716 for (group, (mut fields, members)) in groups.into_iter().enumerate() {
717 let mut group_patch = if group == last_group {
718 std::mem::take(&mut patch)
719 } else {
720 patch.clone()
721 };
722 let merge_once = fields.is_object() && group_patch.is_object();
723 if merge_once {
724 merge_fields(
725 Arc::make_mut(&mut fields),
726 std::mem::take(&mut group_patch),
727 append_paths,
728 max_array_length,
729 );
730 }
731 for index in members {
732 let Some(entity) = caches
733 .get_mut(views[index].view_id)
734 .and_then(|view| view.entities.peek_mut(key))
735 else {
736 continue;
737 };
738 *entity.fields_mut() = fields.clone();
739 if merge_once {
740 let version = entity.version_mut();
741 *version = merge_version(
742 version.take(),
743 version_of(index),
744 append_paths,
745 max_array_length,
746 );
747 } else {
748 merge_shared(
751 entity,
752 with_version(group_patch.clone(), version_of(index)),
753 append_paths,
754 max_array_length,
755 );
756 }
757 }
758 }
759 writes
760 }
761
762 pub async fn store_whole(&self, view_id: &str, key: &str, entity: Value) -> bool {
766 self.store_whole_for_lifetime(view_id, key, entity, None)
767 .await
768 }
769
770 pub async fn store_whole_for_lifetime(
773 &self,
774 view_id: &str,
775 key: &str,
776 entity: Value,
777 requested_account_position: Option<AccountPosition>,
778 ) -> bool {
779 self.store_whole_checked(view_id, key, entity, requested_account_position, None)
780 .await
781 }
782
783 pub async fn store_whole_for_ordering(
784 &self,
785 view_id: &str,
786 key: &str,
787 entity: Value,
788 requested_account_position: Option<AccountPosition>,
789 requested_source_seq: Option<&str>,
790 ) -> bool {
791 self.store_whole_for_ordering_in_domain(
792 view_id,
793 key,
794 entity,
795 requested_account_position,
796 requested_source_seq,
797 SlotIndexDomain::Legacy,
798 )
799 .await
800 }
801
802 pub async fn store_whole_for_ordering_in_domain(
803 &self,
804 view_id: &str,
805 key: &str,
806 entity: Value,
807 requested_account_position: Option<AccountPosition>,
808 requested_source_seq: Option<&str>,
809 requested_source_domain: SlotIndexDomain,
810 ) -> bool {
811 self.store_whole_checked(
812 view_id,
813 key,
814 entity,
815 requested_account_position,
816 Some((requested_source_seq, requested_source_domain)),
817 )
818 .await
819 }
820
821 async fn store_whole_checked(
822 &self,
823 view_id: &str,
824 key: &str,
825 entity: Value,
826 requested_account_position: Option<AccountPosition>,
827 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
828 ) -> bool {
829 let (entity, version) = split_version(entity);
830 self.store_whole_views(
831 key,
832 entity,
833 &[ViewVersion { view_id, version }],
834 requested_account_position,
835 requested_source,
836 )
837 .await
838 .pop()
839 .expect("store_whole_views answers for every view")
840 }
841
842 pub(crate) async fn store_whole_views(
848 &self,
849 key: &str,
850 entity: Value,
851 views: &[ViewVersion<'_>],
852 requested_account_position: Option<AccountPosition>,
853 requested_source: Option<(Option<&str>, SlotIndexDomain)>,
854 ) -> Vec<bool> {
855 let max_array_length = self.config.max_array_length;
856 let (entity, stray_version) = split_version(entity);
857 let fields = Arc::new(Fields::from_value(truncate_arrays_if_needed(
858 entity,
859 max_array_length,
860 )));
861 let mut caches = self.caches.write().await;
862 views
863 .iter()
864 .map(|view| {
865 let entries = caches
866 .entry(view.view_id.to_string())
867 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
868 if !entries.accepts_whole(key, requested_account_position, requested_source) {
869 return false;
870 }
871 let version = view
872 .version
873 .clone()
874 .or_else(|| stray_version.clone())
875 .filter(|_| fields.is_object())
876 .map(|version| truncate_arrays_if_needed(version, max_array_length));
877 entries.store(
878 key.to_string(),
879 SharedEntity::from_parts(fields.clone(), version),
880 );
881 true
882 })
883 .collect()
884 }
885
886 pub async fn account_position(&self, view_id: &str, key: &str) -> Option<AccountPosition> {
887 self.caches
888 .read()
889 .await
890 .get(view_id)
891 .and_then(|view| view.lifetimes.peek(key))
892 .and_then(|checkpoint| checkpoint.account_position)
893 }
894
895 pub async fn accepts_mutation(
896 &self,
897 view_id: &str,
898 key: &str,
899 _entity: &Value,
900 creation: bool,
901 ) -> bool {
902 self.accepts_lifetime_mutation(view_id, key, creation, None)
903 .await
904 }
905
906 pub async fn accepts_lifetime_mutation(
907 &self,
908 view_id: &str,
909 key: &str,
910 creation: bool,
911 account_position: Option<AccountPosition>,
912 ) -> bool {
913 self.accepts_ordered_lifetime_mutation(view_id, key, creation, account_position, None)
914 .await
915 }
916
917 pub async fn accepts_ordered_lifetime_mutation(
918 &self,
919 view_id: &str,
920 key: &str,
921 creation: bool,
922 account_position: Option<AccountPosition>,
923 source_seq: Option<&str>,
924 ) -> bool {
925 self.accepts_ordered_lifetime_mutation_in_domain(
926 view_id,
927 key,
928 creation,
929 account_position,
930 source_seq,
931 SlotIndexDomain::Legacy,
932 )
933 .await
934 }
935
936 pub async fn accepts_ordered_lifetime_mutation_in_domain(
937 &self,
938 view_id: &str,
939 key: &str,
940 creation: bool,
941 account_position: Option<AccountPosition>,
942 source_seq: Option<&str>,
943 source_domain: SlotIndexDomain,
944 ) -> bool {
945 self.caches.read().await.get(view_id).is_none_or(|view| {
946 view.accepts(
947 key,
948 creation,
949 account_position,
950 source_seq,
951 Some(source_domain),
952 )
953 })
954 }
955
956 pub async fn delete(
960 &self,
961 view_id: &str,
962 key: &str,
963 account_position: Option<AccountPosition>,
964 ) -> bool {
965 match account_position {
966 Some(position) => {
967 self.delete_ordered(view_id, key, Some(position), None)
968 .await
969 }
970 None => self.delete_current(view_id, key).await,
971 }
972 }
973
974 pub async fn delete_current(&self, view_id: &str, key: &str) -> bool {
978 let mut caches = self.caches.write().await;
979 let view = caches
980 .entry(view_id.to_string())
981 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
982 let checkpoint = view
983 .lifetimes
984 .peek(key)
985 .cloned()
986 .unwrap_or(EntityLifetimeCheckpoint {
987 account_position: None,
988 source_seq: None,
989 source_sequences: BTreeMap::new(),
990 deleted: false,
991 });
992 if checkpoint.deleted {
993 return true;
994 }
995 view.entities.pop(key);
996 view.forget_evicted(key);
997 view.lifetimes.put(
998 key.to_string(),
999 EntityLifetimeCheckpoint {
1000 deleted: true,
1001 ..checkpoint
1002 },
1003 );
1004 view.trim_lifetimes();
1005 true
1006 }
1007
1008 pub async fn delete_ordered(
1011 &self,
1012 view_id: &str,
1013 key: &str,
1014 account_position: Option<AccountPosition>,
1015 source_seq: Option<&str>,
1016 ) -> bool {
1017 self.delete_ordered_in_domain(
1018 view_id,
1019 key,
1020 account_position,
1021 source_seq,
1022 SlotIndexDomain::Legacy,
1023 )
1024 .await
1025 }
1026
1027 pub async fn delete_ordered_in_domain(
1028 &self,
1029 view_id: &str,
1030 key: &str,
1031 account_position: Option<AccountPosition>,
1032 source_seq: Option<&str>,
1033 source_domain: SlotIndexDomain,
1034 ) -> bool {
1035 let mut caches = self.caches.write().await;
1036 let view = caches
1037 .entry(view_id.to_string())
1038 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1039 if let Some(checkpoint) = view.lifetimes.peek(key) {
1040 match (checkpoint.account_position, account_position) {
1041 (Some(previous), Some(incoming)) if incoming < previous => return false,
1042 (Some(previous), Some(incoming)) if incoming == previous => {
1043 return checkpoint.deleted;
1044 }
1045 _ => {}
1046 }
1047 }
1048 if account_position.is_none()
1049 && source_seq.is_some()
1050 && !view.accepts_source_ordering(key, source_seq, Some(source_domain), true)
1051 {
1052 return false;
1053 }
1054 let previous = view.lifetimes.peek(key).cloned();
1055 view.entities.pop(key);
1056 view.forget_evicted(key);
1057 let mut checkpoint = if account_position.is_some() {
1058 let previous = previous.unwrap_or(EntityLifetimeCheckpoint {
1059 account_position: None,
1060 source_seq: None,
1061 source_sequences: BTreeMap::new(),
1062 deleted: false,
1063 });
1064 EntityLifetimeCheckpoint {
1065 account_position,
1066 source_seq: previous.source_seq,
1067 source_sequences: previous.source_sequences,
1068 deleted: true,
1069 }
1070 } else {
1071 previous.unwrap_or(EntityLifetimeCheckpoint {
1072 account_position: None,
1073 source_seq: None,
1074 source_sequences: BTreeMap::new(),
1075 deleted: false,
1076 })
1077 };
1078 if account_position.is_none() {
1079 if let Some(source_seq) = source_seq {
1080 checkpoint
1081 .source_sequences
1082 .insert(source_domain, source_seq.to_string());
1083 }
1084 checkpoint.deleted = true;
1085 }
1086 view.lifetimes.put(key.to_string(), checkpoint);
1087 view.trim_lifetimes();
1088 true
1089 }
1090
1091 pub async fn dump_lifetimes(&self) -> EntityLifetimes {
1092 self.caches
1093 .read()
1094 .await
1095 .iter()
1096 .flat_map(|(id, view)| {
1097 view.lifetimes
1098 .iter()
1099 .map(|(key, checkpoint)| (id.clone(), key.clone(), checkpoint.clone()))
1100 })
1101 .collect()
1102 }
1103
1104 pub async fn hydrate_lifetimes(&self, lifetimes: EntityLifetimes) {
1105 let mut caches = self.caches.write().await;
1106 for (id, key, checkpoint) in lifetimes.into_iter().rev() {
1107 let view = caches
1108 .entry(id)
1109 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1110 view.lifetimes.put(key, checkpoint);
1111 view.trim_lifetimes();
1112 }
1113 }
1114
1115 pub async fn hydrate_legacy_tombstones(&self, tombstones: EntityTombstones) {
1119 let mut caches = self.caches.write().await;
1120 for (id, key, seq) in tombstones.into_iter().rev() {
1121 let view = caches
1122 .entry(id)
1123 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1124 let deleted = !view.entities.contains(&key);
1125 let account_position = seq.as_deref().and_then(account_position_from_seq);
1126 view.lifetimes.put(
1127 key,
1128 EntityLifetimeCheckpoint {
1129 account_position,
1130 source_seq: None,
1131 source_sequences: BTreeMap::new(),
1132 deleted,
1133 },
1134 );
1135 view.trim_lifetimes();
1136 }
1137 }
1138
1139 pub async fn deletion_is_current(&self, view_id: &str, key: &str) -> bool {
1143 let caches = self.caches.read().await;
1144 caches.get(view_id).is_none_or(|view| {
1145 view.lifetimes
1146 .peek(key)
1147 .map_or(!view.entities.contains(key), |checkpoint| {
1148 checkpoint.deleted
1149 })
1150 })
1151 }
1152
1153 pub fn merge_patch(&self, base: &mut Value, patch: Value, append_paths: &[String]) {
1156 deep_merge_with_append(base, patch, append_paths, self.config.max_array_length);
1157 }
1158
1159 pub fn merge_patch_shared(
1162 &self,
1163 entity: &mut SharedEntity,
1164 patch: Value,
1165 append_paths: &[String],
1166 ) {
1167 merge_shared(entity, patch, append_paths, self.config.max_array_length);
1168 }
1169
1170 pub async fn remember_evicted(&self, view_id: &str, keys: impl IntoIterator<Item = String>) {
1176 let mut caches = self.caches.write().await;
1177 let view = caches
1178 .entry(view_id.to_string())
1179 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1180 for key in keys {
1181 if !view.entities.contains(&key) {
1182 view.remember_evicted(key);
1183 }
1184 }
1185 }
1186
1187 pub async fn get_all(&self, view_id: &str) -> Vec<(String, Value)> {
1193 self.get_all_shared(view_id)
1194 .await
1195 .into_iter()
1196 .map(|(key, entity)| (key, entity.into_value()))
1197 .collect()
1198 }
1199
1200 pub async fn get_all_shared(&self, view_id: &str) -> Vec<(String, SharedEntity)> {
1203 let caches = self.caches.read().await;
1204
1205 caches
1206 .get(view_id)
1207 .map(|view| {
1208 view.entities
1209 .iter()
1210 .map(|(k, v)| (k.clone(), v.clone()))
1211 .collect()
1212 })
1213 .unwrap_or_default()
1214 }
1215
1216 pub async fn get_after(
1222 &self,
1223 view_id: &str,
1224 cursor: &str,
1225 limit: Option<usize>,
1226 ) -> Vec<(String, Value)> {
1227 let caches = self.caches.read().await;
1228
1229 if let Some(view) = caches.get(view_id) {
1230 let mut results: Vec<(String, &SharedEntity)> = view
1231 .entities
1232 .iter()
1233 .filter(|(_, entity)| {
1234 entity
1235 .field("_seq")
1236 .and_then(|s| s.as_str())
1237 .map(|seq| cmp_seq(seq, cursor) == std::cmp::Ordering::Greater)
1238 .unwrap_or(false)
1239 })
1240 .map(|(k, v)| (k.clone(), v))
1241 .collect();
1242
1243 results.sort_by(|a, b| {
1245 let seq_a = a.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1246 let seq_b = b.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1247 cmp_seq(seq_a, seq_b)
1248 });
1249
1250 if let Some(limit) = limit {
1252 results.truncate(limit);
1253 }
1254
1255 results
1256 .into_iter()
1257 .map(|(key, entity)| (key, entity.to_value()))
1258 .collect()
1259 } else {
1260 vec![]
1261 }
1262 }
1263
1264 pub async fn get(&self, view_id: &str, key: &str) -> Option<Value> {
1266 self.get_shared(view_id, key)
1267 .await
1268 .map(SharedEntity::into_value)
1269 }
1270
1271 pub async fn get_shared(&self, view_id: &str, key: &str) -> Option<SharedEntity> {
1274 let caches = self.caches.read().await;
1275 caches
1276 .get(view_id)
1277 .and_then(|view| view.entities.peek(key).cloned())
1278 }
1279
1280 pub async fn remove(&self, view_id: &str, key: &str) -> Option<Value> {
1285 let removed = {
1286 let mut caches = self.caches.write().await;
1287 caches.get_mut(view_id).and_then(|view| {
1288 view.forget_evicted(key);
1289 view.entities.pop(key)
1290 })
1291 };
1292 removed.map(SharedEntity::into_value)
1293 }
1294
1295 pub async fn len(&self, view_id: &str) -> usize {
1297 let caches = self.caches.read().await;
1298 caches
1299 .get(view_id)
1300 .map(|view| view.entities.len())
1301 .unwrap_or(0)
1302 }
1303
1304 pub async fn is_empty(&self, view_id: &str) -> bool {
1306 self.len(view_id).await == 0
1307 }
1308
1309 pub fn snapshot_config(&self) -> SnapshotBatchConfig {
1311 SnapshotBatchConfig {
1312 initial_batch_size: self.config.initial_snapshot_batch_size,
1313 subsequent_batch_size: self.config.subsequent_snapshot_batch_size,
1314 }
1315 }
1316
1317 pub async fn clear(&self, view_id: &str) {
1319 let mut caches = self.caches.write().await;
1320 if let Some(view) = caches.get_mut(view_id) {
1321 view.entities.clear();
1322 view.evicted = None;
1323 }
1324 }
1325
1326 pub async fn clear_all(&self) {
1327 let mut caches = self.caches.write().await;
1328 caches.clear();
1329 }
1330
1331 pub async fn dump(&self) -> Vec<(String, Vec<(String, Value)>)> {
1337 self.dump_shared()
1338 .await
1339 .into_iter()
1340 .map(|(view_id, entries)| {
1341 (
1342 view_id,
1343 entries
1344 .into_iter()
1345 .map(|(key, entity)| (key, entity.into_value()))
1346 .collect(),
1347 )
1348 })
1349 .collect()
1350 }
1351
1352 pub async fn dump_shared(&self) -> Vec<(String, Vec<(String, SharedEntity)>)> {
1355 let caches = self.caches.read().await;
1356 caches
1357 .iter()
1358 .map(|(view_id, view)| {
1359 (
1360 view_id.clone(),
1361 view.entities
1362 .iter()
1363 .map(|(key, entity)| (key.clone(), entity.clone()))
1364 .collect(),
1365 )
1366 })
1367 .collect()
1368 }
1369
1370 pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
1378 let mut caches = self.caches.write().await;
1379 caches.clear();
1380 let mut restored: HashMap<(String, String), Vec<Arc<Fields>>> = HashMap::new();
1381 for (view_id, entries) in views {
1382 let export = view_id.split('/').next().unwrap_or_default().to_string();
1383 let view = caches
1384 .entry(view_id)
1385 .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1386 for (key, entity) in entries.into_iter().rev() {
1387 let (fields, version) = SharedEntity::new(entity).into_parts();
1388 let seen = restored.entry((export.clone(), key.clone())).or_default();
1389 let fields = match seen.iter().find(|held| **held == fields) {
1390 Some(held) => held.clone(),
1391 None => {
1392 seen.push(fields.clone());
1393 fields
1394 }
1395 };
1396 view.store(key, SharedEntity::from_parts(fields, version));
1397 }
1398 }
1399 }
1400
1401 pub async fn stats(&self) -> CacheStats {
1402 let caches = self.caches.read().await;
1403 let mut total_entities = 0;
1404 let mut views = Vec::new();
1405
1406 for (view_id, view) in caches.iter() {
1407 let count = view.entities.len();
1408 total_entities += count;
1409 views.push((view_id.clone(), count));
1410 }
1411
1412 views.sort_by_key(|b| std::cmp::Reverse(b.1));
1413
1414 CacheStats {
1415 view_count: caches.len(),
1416 total_entities,
1417 top_views: views.into_iter().take(5).collect(),
1418 }
1419 }
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Eq)]
1424pub struct CacheStats {
1425 pub view_count: usize,
1426 pub total_entities: usize,
1427 pub top_views: Vec<(String, usize)>,
1428}
1429
1430#[derive(Debug, Clone, Copy)]
1431pub struct SnapshotBatchConfig {
1432 pub initial_batch_size: usize,
1433 pub subsequent_batch_size: usize,
1434}
1435
1436impl Default for EntityCache {
1437 fn default() -> Self {
1438 Self::new()
1439 }
1440}
1441
1442fn deep_merge_with_append(
1443 base: &mut Value,
1444 patch: Value,
1445 append_paths: &[String],
1446 max_array_length: usize,
1447) {
1448 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
1449}
1450
1451fn deep_merge_with_append_inner(
1452 base: &mut Value,
1453 patch: Value,
1454 append_paths: &[String],
1455 current_path: &str,
1456 max_array_length: usize,
1457) {
1458 match (base, patch) {
1459 (Value::Object(base_map), Value::Object(patch_map)) => {
1460 for (key, patch_value) in patch_map {
1461 let child_path = child_path(append_paths, current_path, &key);
1462
1463 if let Some(base_value) = base_map.get_mut(&key) {
1464 deep_merge_with_append_inner(
1465 base_value,
1466 patch_value,
1467 append_paths,
1468 &child_path,
1469 max_array_length,
1470 );
1471 } else {
1472 base_map.insert(
1473 key,
1474 truncate_arrays_if_needed(patch_value, max_array_length),
1475 );
1476 }
1477 }
1478 }
1479
1480 (Value::Array(base_arr), Value::Array(patch_arr)) => {
1481 let should_append = append_paths.iter().any(|p| p == current_path);
1482 if should_append {
1483 base_arr.extend(patch_arr);
1484 if base_arr.len() > max_array_length {
1485 let excess = base_arr.len() - max_array_length;
1486 base_arr.drain(0..excess);
1487 }
1488 } else {
1489 *base_arr = patch_arr;
1490 if base_arr.len() > max_array_length {
1491 let excess = base_arr.len() - max_array_length;
1492 base_arr.drain(0..excess);
1493 }
1494 }
1495 }
1496
1497 (base, patch_value) => {
1498 *base = truncate_arrays_if_needed(patch_value, max_array_length);
1499 }
1500 }
1501}
1502
1503fn merge_fields(
1508 fields: &mut Fields,
1509 patch: Value,
1510 append_paths: &[String],
1511 max_array_length: usize,
1512) {
1513 let (Fields::Object(members), Value::Object(patch)) = (fields, patch) else {
1514 unreachable!("merge_fields merges an object patch into an object");
1515 };
1516 for (key, patch_value) in patch {
1517 match members.get_mut(&key) {
1518 Some(member) => {
1519 if merge_changes_nothing(member, &patch_value, append_paths, &key, max_array_length)
1520 {
1521 continue;
1522 }
1523 match Arc::get_mut(member) {
1524 Some(value) => deep_merge_with_append_inner(
1525 value,
1526 patch_value,
1527 append_paths,
1528 &key,
1529 max_array_length,
1530 ),
1531 None => {
1532 *member = Arc::new(merged_copy(
1533 member,
1534 patch_value,
1535 append_paths,
1536 &key,
1537 max_array_length,
1538 ))
1539 }
1540 }
1541 }
1542 None => members.insert(
1543 key,
1544 Arc::new(truncate_arrays_if_needed(patch_value, max_array_length)),
1545 ),
1546 }
1547 }
1548}
1549
1550fn merge_changes_nothing(
1553 base: &Value,
1554 patch: &Value,
1555 append_paths: &[String],
1556 current_path: &str,
1557 max_array_length: usize,
1558) -> bool {
1559 match (base, patch) {
1560 (Value::Object(base_map), Value::Object(patch_map)) => {
1561 patch_map.iter().all(|(key, patch_value)| {
1562 base_map.get(key).is_some_and(|base_value| {
1563 merge_changes_nothing(
1564 base_value,
1565 patch_value,
1566 append_paths,
1567 &child_path(append_paths, current_path, key),
1568 max_array_length,
1569 )
1570 })
1571 })
1572 }
1573 (Value::Array(base_arr), Value::Array(patch_arr)) => {
1574 base_arr.len() <= max_array_length
1575 && if append_paths.iter().any(|path| path == current_path) {
1576 patch_arr.is_empty()
1577 } else {
1578 base_arr == patch_arr
1579 }
1580 }
1581 (Value::Object(_) | Value::Array(_), _) | (_, Value::Object(_) | Value::Array(_)) => false,
1582 (base, patch) => base == patch,
1583 }
1584}
1585
1586fn merged_copy(
1589 base: &Value,
1590 patch: Value,
1591 append_paths: &[String],
1592 current_path: &str,
1593 max_array_length: usize,
1594) -> Value {
1595 match (base, patch) {
1596 (Value::Object(base_map), Value::Object(patch_map)) => {
1597 let mut merged = serde_json::Map::new();
1600 for (key, value) in base_map {
1601 let value = if patch_map.contains_key(key) {
1602 Value::Null
1603 } else {
1604 value.clone()
1605 };
1606 merged.insert(key.clone(), value);
1607 }
1608 for (key, patch_value) in patch_map {
1609 let child_path = child_path(append_paths, current_path, &key);
1610 match (merged.get_mut(&key), base_map.get(&key)) {
1611 (Some(slot), Some(base_value)) => {
1612 *slot = merged_copy(
1613 base_value,
1614 patch_value,
1615 append_paths,
1616 &child_path,
1617 max_array_length,
1618 );
1619 }
1620 _ => {
1621 merged.insert(
1622 key,
1623 truncate_arrays_if_needed(patch_value, max_array_length),
1624 );
1625 }
1626 }
1627 }
1628 Value::Object(merged)
1629 }
1630 (Value::Array(base_arr), Value::Array(mut patch_arr)) => {
1631 if append_paths.iter().any(|path| path == current_path) {
1632 let kept = max_array_length
1634 .saturating_sub(patch_arr.len())
1635 .min(base_arr.len());
1636 let mut merged = base_arr[base_arr.len() - kept..].to_vec();
1637 merged.append(&mut patch_arr);
1638 if merged.len() > max_array_length {
1639 let excess = merged.len() - max_array_length;
1640 merged.drain(0..excess);
1641 }
1642 Value::Array(merged)
1643 } else {
1644 if patch_arr.len() > max_array_length {
1645 let excess = patch_arr.len() - max_array_length;
1646 patch_arr.drain(0..excess);
1647 }
1648 Value::Array(patch_arr)
1649 }
1650 }
1651 (_, patch_value) => truncate_arrays_if_needed(patch_value, max_array_length),
1652 }
1653}
1654
1655fn child_path(append_paths: &[String], current_path: &str, key: &str) -> String {
1658 if append_paths.is_empty() {
1659 String::new()
1660 } else if current_path.is_empty() {
1661 key.to_string()
1662 } else {
1663 format!("{current_path}.{key}")
1664 }
1665}
1666
1667fn truncate_arrays_if_needed(mut value: Value, max_array_length: usize) -> Value {
1670 truncate_arrays_in_place(&mut value, max_array_length);
1671 value
1672}
1673
1674fn truncate_arrays_in_place(value: &mut Value, max_array_length: usize) {
1677 match value {
1678 Value::Array(arr) => {
1679 if arr.len() > max_array_length {
1680 let excess = arr.len() - max_array_length;
1681 arr.drain(0..excess);
1682 }
1683 for element in arr {
1684 truncate_arrays_in_place(element, max_array_length);
1685 }
1686 }
1687 Value::Object(map) => {
1688 for field in map.values_mut() {
1689 truncate_arrays_in_place(field, max_array_length);
1690 }
1691 }
1692 _ => {}
1693 }
1694}
1695
1696fn merge_shared(
1700 entity: &mut SharedEntity,
1701 patch: Value,
1702 append_paths: &[String],
1703 max_array_length: usize,
1704) {
1705 let (patch, patch_version) = split_version(patch);
1706 if entity.fields().is_object() && patch.is_object() {
1707 merge_fields(
1708 Arc::make_mut(entity.fields_mut()),
1709 patch,
1710 append_paths,
1711 max_array_length,
1712 );
1713 let version = entity.version_mut();
1714 *version = merge_version(
1715 version.take(),
1716 patch_version,
1717 append_paths,
1718 max_array_length,
1719 );
1720 return;
1721 }
1722 let (fields, _) = std::mem::replace(entity, SharedEntity::new(Value::Null)).into_parts();
1726 let mut whole = match (&*fields, &patch) {
1727 (Fields::Other(Value::Array(_)), Value::Array(_)) => match Arc::try_unwrap(fields) {
1728 Ok(fields) => fields.into_value(),
1729 Err(shared) => shared.to_value(),
1730 },
1731 _ => Value::Null,
1732 };
1733 deep_merge_with_append(
1734 &mut whole,
1735 with_version(patch, patch_version),
1736 append_paths,
1737 max_array_length,
1738 );
1739 *entity = SharedEntity::new(whole);
1740}
1741
1742fn merge_version(
1745 current: Option<Value>,
1746 incoming: Option<Value>,
1747 append_paths: &[String],
1748 max_array_length: usize,
1749) -> Option<Value> {
1750 match (current, incoming) {
1751 (Some(mut current), Some(incoming)) => {
1752 deep_merge_with_append_inner(
1753 &mut current,
1754 incoming,
1755 append_paths,
1756 VERSION_FIELD,
1757 max_array_length,
1758 );
1759 Some(current)
1760 }
1761 (None, Some(incoming)) => Some(truncate_arrays_if_needed(incoming, max_array_length)),
1762 (current, None) => current,
1763 }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769 use serde_json::json;
1770
1771 #[tokio::test]
1772 async fn same_slot_instruction_activity_cannot_block_authoritative_deletion() {
1773 let cache = EntityCache::new();
1774 assert_eq!(
1775 cache
1776 .upsert_with_lifetime(
1777 "v",
1778 "a",
1779 json!({"account":true,"_seq":"100:000000000009"}),
1780 &[],
1781 PatchOrigin::Creation,
1782 Some(AccountPosition::new(100, 9)),
1783 )
1784 .await,
1785 CacheWrite::Created
1786 );
1787 assert_eq!(
1788 cache
1789 .upsert_with_append(
1790 "v",
1791 "a",
1792 json!({"instruction":true,"_seq":"100:000000000900"}),
1793 &[],
1794 PatchOrigin::Change
1795 )
1796 .await,
1797 CacheWrite::Merged
1798 );
1799 assert!(
1800 cache
1801 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1802 .await
1803 );
1804 assert!(cache.get("v", "a").await.is_none());
1805 assert!(cache.deletion_is_current("v", "a").await);
1806 }
1807
1808 #[tokio::test]
1809 async fn recreation_has_its_own_ordering_and_replaces_old_fields() {
1810 let cache = EntityCache::with_config(EntityCacheConfig {
1811 max_entities_per_view: 1,
1812 ..Default::default()
1813 });
1814 assert!(
1815 cache
1816 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1817 .await
1818 );
1819 assert_eq!(
1820 cache
1821 .upsert_with_lifetime(
1822 "v",
1823 "a",
1824 json!({"fresh":true, "_seq":"100:000000001002"}),
1825 &[],
1826 PatchOrigin::Creation,
1827 Some(AccountPosition::new(100, 1002)),
1828 )
1829 .await,
1830 CacheWrite::Created
1831 );
1832 assert_eq!(
1833 cache
1834 .upsert_with_append(
1835 "v",
1836 "a",
1837 json!({"instruction":true, "_seq":"100:000000000900"}),
1838 &[],
1839 PatchOrigin::Change
1840 )
1841 .await,
1842 CacheWrite::Merged
1843 );
1844
1845 assert!(matches!(
1848 cache
1849 .upsert_with_lifetime(
1850 "v",
1851 "a",
1852 json!({"old":true, "_seq":"100:000000001001"}),
1853 &[],
1854 PatchOrigin::Change,
1855 Some(AccountPosition::new(100, 1001)),
1856 )
1857 .await,
1858 CacheWrite::Refused { .. }
1859 ));
1860 assert!(
1861 !cache
1862 .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1863 .await
1864 );
1865 assert!(
1866 !cache
1867 .store_whole("v", "a", json!({"old":true, "_seq":"100:000000000900"}))
1868 .await
1869 );
1870 let row = cache.get("v", "a").await.unwrap();
1871 assert_eq!(row["fresh"], true);
1872 assert_eq!(row["instruction"], true);
1873 assert!(row.get("old").is_none());
1874
1875 cache
1876 .upsert_with_append(
1877 "v",
1878 "other",
1879 json!({"other":true}),
1880 &[],
1881 PatchOrigin::Creation,
1882 )
1883 .await;
1884 assert!(cache.get("v", "a").await.is_none());
1885 assert!(matches!(
1886 cache
1887 .upsert_with_lifetime(
1888 "v",
1889 "a",
1890 json!({"accountUpdate":true, "_seq":"100:000000001003"}),
1891 &[],
1892 PatchOrigin::Change,
1893 Some(AccountPosition::new(100, 1003)),
1894 )
1895 .await,
1896 CacheWrite::Refused { .. }
1897 ));
1898 assert!(
1899 !cache
1900 .store_whole_for_lifetime(
1901 "v",
1902 "a",
1903 row.clone(),
1904 Some(AccountPosition::new(100, 1002)),
1905 )
1906 .await
1907 );
1908 let mut current = row;
1909 current["accountUpdate"] = Value::Bool(true);
1910 current["_seq"] = Value::String("100:000000001003".into());
1911 assert!(
1912 cache
1913 .store_whole_for_lifetime("v", "a", current, Some(AccountPosition::new(100, 1003)),)
1914 .await
1915 );
1916
1917 let restored = EntityCache::new();
1920 restored.hydrate(cache.dump().await).await;
1921 restored
1922 .hydrate_lifetimes(cache.dump_lifetimes().await)
1923 .await;
1924 assert_eq!(
1925 restored
1926 .upsert_with_append(
1927 "v",
1928 "a",
1929 json!({"afterRestore":true, "_seq":"100:000000000901"}),
1930 &[],
1931 PatchOrigin::Change,
1932 )
1933 .await,
1934 CacheWrite::Merged
1935 );
1936 assert!(matches!(
1937 restored
1938 .upsert_with_lifetime(
1939 "v",
1940 "a",
1941 json!({"old":true, "_seq":"100:000000001001"}),
1942 &[],
1943 PatchOrigin::Change,
1944 Some(AccountPosition::new(100, 1001)),
1945 )
1946 .await,
1947 CacheWrite::Refused { .. }
1948 ));
1949 }
1950
1951 #[tokio::test]
1952 async fn recreation_retains_each_source_cursor_as_an_old_lifetime_barrier() {
1953 let cache = EntityCache::new();
1954 assert_eq!(
1955 cache
1956 .upsert_with_lifetime(
1957 "v",
1958 "a",
1959 json!({"lifetime":"old"}),
1960 &[],
1961 PatchOrigin::Creation,
1962 Some(AccountPosition::new(100, 9)),
1963 )
1964 .await,
1965 CacheWrite::Created
1966 );
1967 for (domain, seq, field) in [
1968 (
1969 SlotIndexDomain::Instruction,
1970 "100:000000000800",
1971 "oldInstruction",
1972 ),
1973 (SlotIndexDomain::Resolver, "100:900000000000", "oldResolver"),
1974 ] {
1975 let mut patch = json!({"_seq":seq});
1976 patch[field] = Value::Bool(true);
1977 assert_eq!(
1978 cache
1979 .upsert_with_ordering(
1980 "v",
1981 "a",
1982 patch,
1983 &[],
1984 PatchOrigin::Change,
1985 LifetimeOrdering {
1986 account_position: None,
1987 source_seq: Some(seq),
1988 source_domain: Some(domain),
1989 },
1990 )
1991 .await,
1992 CacheWrite::Merged
1993 );
1994 }
1995
1996 assert!(
1997 cache
1998 .delete("v", "a", Some(AccountPosition::new(100, 10)))
1999 .await
2000 );
2001 assert_eq!(
2002 cache
2003 .upsert_with_lifetime(
2004 "v",
2005 "a",
2006 json!({"lifetime":"new"}),
2007 &[],
2008 PatchOrigin::Creation,
2009 Some(AccountPosition::new(100, 11)),
2010 )
2011 .await,
2012 CacheWrite::Created
2013 );
2014
2015 for (domain, seq, field) in [
2018 (
2019 SlotIndexDomain::Instruction,
2020 "100:000000000799",
2021 "staleInstruction",
2022 ),
2023 (
2024 SlotIndexDomain::Resolver,
2025 "100:899999999999",
2026 "staleResolver",
2027 ),
2028 ] {
2029 let mut patch = json!({"_seq":seq});
2030 patch[field] = Value::Bool(true);
2031 assert!(matches!(
2032 cache
2033 .upsert_with_ordering(
2034 "v",
2035 "a",
2036 patch,
2037 &[],
2038 PatchOrigin::Change,
2039 LifetimeOrdering {
2040 account_position: None,
2041 source_seq: Some(seq),
2042 source_domain: Some(domain),
2043 },
2044 )
2045 .await,
2046 CacheWrite::Refused { .. }
2047 ));
2048 }
2049
2050 assert_eq!(
2053 cache
2054 .upsert_with_ordering(
2055 "v",
2056 "a",
2057 json!({"newInstruction":true, "_seq":"100:000000000900"}),
2058 &[],
2059 PatchOrigin::Change,
2060 LifetimeOrdering {
2061 account_position: None,
2062 source_seq: Some("100:000000000900"),
2063 source_domain: Some(SlotIndexDomain::Instruction),
2064 },
2065 )
2066 .await,
2067 CacheWrite::Merged
2068 );
2069 let row = cache.get("v", "a").await.unwrap();
2070 assert_eq!(row["lifetime"], "new");
2071 assert_eq!(row["newInstruction"], true);
2072 assert!(row.get("oldInstruction").is_none());
2073 assert!(row.get("oldResolver").is_none());
2074 assert!(row.get("staleInstruction").is_none());
2075 assert!(row.get("staleResolver").is_none());
2076
2077 let restored = EntityCache::new();
2078 restored.hydrate(cache.dump().await).await;
2079 restored
2080 .hydrate_lifetimes(cache.dump_lifetimes().await)
2081 .await;
2082 assert!(matches!(
2083 restored
2084 .upsert_with_ordering(
2085 "v",
2086 "a",
2087 json!({"staleAfterRestore":true}),
2088 &[],
2089 PatchOrigin::Change,
2090 LifetimeOrdering {
2091 account_position: None,
2092 source_seq: Some("100:000000000799"),
2093 source_domain: Some(SlotIndexDomain::Instruction),
2094 },
2095 )
2096 .await,
2097 CacheWrite::Refused { .. }
2098 ));
2099 }
2100
2101 #[tokio::test]
2102 async fn markerless_sources_keep_seq_ordering_across_recreation_and_restore() {
2103 let cache = EntityCache::new();
2104 assert_eq!(
2105 cache
2106 .upsert_with_append(
2107 "v",
2108 "a",
2109 json!({"old":true,"_seq":"100:000000000010"}),
2110 &[],
2111 PatchOrigin::Creation,
2112 )
2113 .await,
2114 CacheWrite::Created
2115 );
2116 assert!(
2117 !cache
2118 .delete_ordered("v", "a", None, Some("100:000000000009"))
2119 .await
2120 );
2121 assert!(cache.get("v", "a").await.is_some());
2122 assert!(
2123 cache
2124 .delete_ordered("v", "a", None, Some("100:000000000011"))
2125 .await
2126 );
2127 assert_eq!(
2128 cache
2129 .upsert_with_append(
2130 "v",
2131 "a",
2132 json!({"fresh":true,"_seq":"100:000000000012"}),
2133 &[],
2134 PatchOrigin::Creation,
2135 )
2136 .await,
2137 CacheWrite::Created
2138 );
2139 assert!(matches!(
2140 cache
2141 .upsert_with_append(
2142 "v",
2143 "a",
2144 json!({"stale":true,"_seq":"100:000000000010"}),
2145 &[],
2146 PatchOrigin::Creation,
2147 )
2148 .await,
2149 CacheWrite::Refused { .. }
2150 ));
2151 assert!(
2152 !cache
2153 .delete_ordered("v", "a", None, Some("100:000000000011"))
2154 .await
2155 );
2156
2157 let restored = EntityCache::new();
2158 restored.hydrate(cache.dump().await).await;
2159 restored
2160 .hydrate_lifetimes(cache.dump_lifetimes().await)
2161 .await;
2162 assert!(
2163 !restored
2164 .delete_ordered("v", "a", None, Some("100:000000000011"))
2165 .await
2166 );
2167 let row = restored.get("v", "a").await.unwrap();
2168 assert_eq!(row["fresh"], true);
2169 assert!(row.get("old").is_none());
2170 assert!(row.get("stale").is_none());
2171
2172 let resync_cache = EntityCache::new();
2175 resync_cache
2176 .hydrate_lifetimes(cache.dump_lifetimes().await)
2177 .await;
2178 assert!(
2179 !resync_cache
2180 .store_whole_for_ordering(
2181 "v",
2182 "a",
2183 json!({"stale_resend":true}),
2184 None,
2185 Some("100:000000000010"),
2186 )
2187 .await
2188 );
2189 assert!(
2190 resync_cache
2191 .store_whole_for_ordering(
2192 "v",
2193 "a",
2194 json!({"fresh_resend":true}),
2195 None,
2196 Some("100:000000000012"),
2197 )
2198 .await
2199 );
2200 }
2201
2202 #[tokio::test]
2203 async fn older_source_patch_cannot_replace_newer_row_or_move_its_cursor_back() {
2204 let cache = EntityCache::new();
2205 assert_eq!(
2206 cache
2207 .upsert_with_append(
2208 "v",
2209 "a",
2210 json!({"value":"created","_seq":"100:000000000010"}),
2211 &[],
2212 PatchOrigin::Creation,
2213 )
2214 .await,
2215 CacheWrite::Created
2216 );
2217 assert_eq!(
2218 cache
2219 .upsert_with_ordering(
2220 "v",
2221 "a",
2222 json!({"value":"newer","_seq":"100:000000000012"}),
2223 &[],
2224 PatchOrigin::Change,
2225 LifetimeOrdering {
2226 account_position: None,
2227 source_seq: Some("100:000000000012"),
2228 source_domain: Some(SlotIndexDomain::Legacy),
2229 },
2230 )
2231 .await,
2232 CacheWrite::Merged
2233 );
2234 assert!(matches!(
2235 cache
2236 .upsert_with_ordering(
2237 "v",
2238 "a",
2239 json!({"value":"older","stale":true,"_seq":"100:000000000011"}),
2240 &[],
2241 PatchOrigin::Change,
2242 LifetimeOrdering {
2243 account_position: None,
2244 source_seq: Some("100:000000000011"),
2245 source_domain: Some(SlotIndexDomain::Legacy),
2246 },
2247 )
2248 .await,
2249 CacheWrite::Refused { .. }
2250 ));
2251 assert!(
2252 !cache
2253 .delete_ordered("v", "a", None, Some("100:000000000011"))
2254 .await
2255 );
2256 let row = cache.get("v", "a").await.unwrap();
2257 assert_eq!(row["value"], "newer");
2258 assert_eq!(row["_seq"], "100:000000000012");
2259 assert!(row.get("stale").is_none());
2260 }
2261
2262 #[tokio::test]
2263 async fn independent_source_recency_domains_survive_snapshot_restore() {
2264 let cache = EntityCache::new();
2265 cache
2266 .upsert_with_lifetime(
2267 "v",
2268 "a",
2269 json!({"value":"created"}),
2270 &[],
2271 PatchOrigin::Creation,
2272 Some(AccountPosition::new(100, 9)),
2273 )
2274 .await;
2275 for (domain, seq, value) in [
2276 (SlotIndexDomain::Resolver, "100:900000000000", "resolver"),
2277 (
2278 SlotIndexDomain::Instruction,
2279 "100:000000000900",
2280 "instruction",
2281 ),
2282 ] {
2283 assert_eq!(
2284 cache
2285 .upsert_with_ordering(
2286 "v",
2287 "a",
2288 json!({"value":value, "_seq":seq}),
2289 &[],
2290 PatchOrigin::Change,
2291 LifetimeOrdering {
2292 account_position: None,
2293 source_seq: Some(seq),
2294 source_domain: Some(domain),
2295 },
2296 )
2297 .await,
2298 CacheWrite::Merged
2299 );
2300 }
2301
2302 let restored = EntityCache::new();
2303 restored.hydrate(cache.dump().await).await;
2304 restored
2305 .hydrate_lifetimes(cache.dump_lifetimes().await)
2306 .await;
2307
2308 for (domain, seq) in [
2309 (SlotIndexDomain::Instruction, "100:000000000899"),
2310 (SlotIndexDomain::Resolver, "100:899999999999"),
2311 ] {
2312 assert!(matches!(
2313 restored
2314 .upsert_with_ordering(
2315 "v",
2316 "a",
2317 json!({"stale":true, "_seq":seq}),
2318 &[],
2319 PatchOrigin::Change,
2320 LifetimeOrdering {
2321 account_position: None,
2322 source_seq: Some(seq),
2323 source_domain: Some(domain),
2324 },
2325 )
2326 .await,
2327 CacheWrite::Refused { .. }
2328 ));
2329 }
2330 assert_eq!(
2331 restored.get("v", "a").await.unwrap()["value"],
2332 "instruction"
2333 );
2334 }
2335
2336 #[tokio::test]
2337 async fn explicit_current_delete_accepts_a_source_without_position() {
2338 let cache = EntityCache::new();
2339 cache
2340 .upsert_with_append(
2341 "v",
2342 "a",
2343 json!({"value":true,"_seq":"100:000000000010"}),
2344 &[],
2345 PatchOrigin::Creation,
2346 )
2347 .await;
2348
2349 assert!(cache.delete("v", "a", None).await);
2350 assert!(cache.get("v", "a").await.is_none());
2351 assert!(cache.deletion_is_current("v", "a").await);
2352 }
2353
2354 #[tokio::test]
2355 async fn evicted_live_row_does_not_make_an_earlier_delete_current() {
2356 let cache = EntityCache::with_config(EntityCacheConfig {
2357 max_entities_per_view: 1,
2358 ..Default::default()
2359 });
2360 cache
2361 .upsert_with_lifetime(
2362 "v",
2363 "a",
2364 json!({"value":true,"_seq":"100:000000000010"}),
2365 &[],
2366 PatchOrigin::Creation,
2367 Some(AccountPosition::new(100, 10)),
2368 )
2369 .await;
2370 cache
2371 .upsert_with_append("v", "b", json!({"value":true}), &[], PatchOrigin::Creation)
2372 .await;
2373
2374 assert!(cache.get("v", "a").await.is_none());
2375 assert!(!cache.deletion_is_current("v", "a").await);
2376 assert!(cache.delete_current("v", "a").await);
2377 assert!(cache.deletion_is_current("v", "a").await);
2378 }
2379
2380 #[tokio::test]
2381 async fn test_basic_upsert_and_get() {
2382 let cache = EntityCache::new();
2383
2384 cache
2385 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
2386 .await;
2387
2388 let entity = cache.get("tokens/list", "abc123").await;
2389 assert!(entity.is_some());
2390 assert_eq!(entity.unwrap()["name"], "Test Token");
2391 }
2392
2393 #[tokio::test]
2394 async fn test_deep_merge_objects() {
2395 let cache = EntityCache::new();
2396
2397 cache
2398 .upsert(
2399 "tokens/list",
2400 "abc123",
2401 json!({
2402 "id": "abc123",
2403 "metrics": {"volume": 100}
2404 }),
2405 )
2406 .await;
2407
2408 cache
2409 .upsert(
2410 "tokens/list",
2411 "abc123",
2412 json!({
2413 "metrics": {"trades": 50}
2414 }),
2415 )
2416 .await;
2417
2418 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2419 assert_eq!(entity["id"], "abc123");
2420 assert_eq!(entity["metrics"]["volume"], 100);
2421 assert_eq!(entity["metrics"]["trades"], 50);
2422 }
2423
2424 #[tokio::test]
2425 async fn test_array_append() {
2426 let cache = EntityCache::new();
2427
2428 cache
2429 .upsert(
2430 "tokens/list",
2431 "abc123",
2432 json!({
2433 "events": [{"type": "buy", "amount": 100}]
2434 }),
2435 )
2436 .await;
2437
2438 cache
2439 .upsert_with_append(
2440 "tokens/list",
2441 "abc123",
2442 json!({
2443 "events": [{"type": "sell", "amount": 50}]
2444 }),
2445 &["events".to_string()],
2446 PatchOrigin::Unknown,
2447 )
2448 .await;
2449
2450 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2451 let events = entity["events"].as_array().unwrap();
2452 assert_eq!(events.len(), 2);
2453 assert_eq!(events[0]["type"], "buy");
2454 assert_eq!(events[1]["type"], "sell");
2455 }
2456
2457 #[tokio::test]
2458 async fn test_array_lru_eviction() {
2459 let config = EntityCacheConfig {
2460 max_entities_per_view: 1000,
2461 max_array_length: 3,
2462 ..Default::default()
2463 };
2464 let cache = EntityCache::with_config(config);
2465
2466 cache
2467 .upsert(
2468 "tokens/list",
2469 "abc123",
2470 json!({
2471 "events": [
2472 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
2473 ]
2474 }),
2475 )
2476 .await;
2477
2478 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2479 let events = entity["events"].as_array().unwrap();
2480
2481 assert_eq!(events.len(), 3);
2482 assert_eq!(events[0]["id"], 3);
2483 assert_eq!(events[1]["id"], 4);
2484 assert_eq!(events[2]["id"], 5);
2485 }
2486
2487 #[tokio::test]
2488 async fn test_array_append_with_lru() {
2489 let config = EntityCacheConfig {
2490 max_entities_per_view: 1000,
2491 max_array_length: 3,
2492 ..Default::default()
2493 };
2494 let cache = EntityCache::with_config(config);
2495
2496 cache
2497 .upsert(
2498 "tokens/list",
2499 "abc123",
2500 json!({
2501 "events": [{"id": 1}, {"id": 2}]
2502 }),
2503 )
2504 .await;
2505
2506 cache
2507 .upsert_with_append(
2508 "tokens/list",
2509 "abc123",
2510 json!({
2511 "events": [{"id": 3}, {"id": 4}]
2512 }),
2513 &["events".to_string()],
2514 PatchOrigin::Unknown,
2515 )
2516 .await;
2517
2518 let entity = cache.get("tokens/list", "abc123").await.unwrap();
2519 let events = entity["events"].as_array().unwrap();
2520
2521 assert_eq!(events.len(), 3);
2523 assert_eq!(events[0]["id"], 2);
2524 assert_eq!(events[1]["id"], 3);
2525 assert_eq!(events[2]["id"], 4);
2526 }
2527
2528 #[tokio::test]
2529 async fn test_entity_lru_eviction() {
2530 let config = EntityCacheConfig {
2531 max_entities_per_view: 2,
2532 max_array_length: 100,
2533 ..Default::default()
2534 };
2535 let cache = EntityCache::with_config(config);
2536
2537 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2538 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2539 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
2540
2541 assert!(cache.get("tokens/list", "key1").await.is_none());
2542 assert!(cache.get("tokens/list", "key2").await.is_some());
2543 assert!(cache.get("tokens/list", "key3").await.is_some());
2544 }
2545
2546 #[tokio::test]
2547 async fn a_patch_for_an_evicted_key_is_refused_not_stored() {
2548 let cache = EntityCache::with_config(EntityCacheConfig {
2549 max_entities_per_view: 2,
2550 ..Default::default()
2551 });
2552 for id in 1..=3 {
2553 assert_eq!(
2554 cache
2555 .upsert(
2556 "tokens/list",
2557 &format!("key{id}"),
2558 json!({"id": id, "n": 0})
2559 )
2560 .await,
2561 CacheWrite::Created
2562 );
2563 }
2564 assert_eq!(
2566 cache.upsert("tokens/list", "key1", json!({"n": 5})).await,
2567 CacheWrite::Refused {
2568 patch: json!({"n": 5})
2569 }
2570 );
2571 assert!(cache.get("tokens/list", "key1").await.is_none());
2572 assert_eq!(
2573 cache.upsert("tokens/list", "key3", json!({"n": 1})).await,
2574 CacheWrite::Merged
2575 );
2576
2577 cache.remove("tokens/list", "key1").await;
2579 assert_eq!(
2580 cache
2581 .upsert("tokens/list", "key1", json!({"id": 1, "n": 6}))
2582 .await,
2583 CacheWrite::Created
2584 );
2585 }
2586
2587 #[tokio::test]
2588 async fn a_whole_entity_ends_an_eviction() {
2589 let cache = EntityCache::with_config(EntityCacheConfig {
2590 max_entities_per_view: 1,
2591 ..Default::default()
2592 });
2593 cache.upsert("v", "a", json!({"id": "a", "n": 0})).await;
2594 cache.upsert("v", "b", json!({"id": "b", "n": 0})).await;
2595 assert!(matches!(
2596 cache.upsert("v", "a", json!({"n": 1})).await,
2597 CacheWrite::Refused { .. }
2598 ));
2599 cache
2600 .store_whole("v", "a", json!({"id": "a", "n": 1}))
2601 .await;
2602 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2603 assert_eq!(cache.len("v").await, 1, "still bounded: b made room");
2604 assert_eq!(
2605 cache.upsert("v", "a", json!({"n": 2})).await,
2606 CacheWrite::Merged
2607 );
2608 }
2609
2610 #[tokio::test]
2611 async fn the_evicted_key_memory_is_bounded() {
2612 let cache = EntityCache::with_config(EntityCacheConfig {
2613 max_entities_per_view: 1,
2614 ..Default::default()
2615 });
2616 let remembered = EVICTED_KEYS_PER_CACHED_ENTITY;
2617 for key in 0..=remembered + 1 {
2618 cache
2619 .upsert("v", &key.to_string(), json!({"id": key}))
2620 .await;
2621 }
2622 assert_eq!(
2625 cache.upsert("v", "0", json!({"id": 0})).await,
2626 CacheWrite::Created
2627 );
2628 assert!(matches!(
2629 cache
2630 .upsert("v", &remembered.to_string(), json!({"n": 1}))
2631 .await,
2632 CacheWrite::Refused { .. }
2633 ));
2634 }
2635
2636 async fn write(
2637 cache: &EntityCache,
2638 key: &str,
2639 patch: Value,
2640 origin: PatchOrigin,
2641 ) -> CacheWrite {
2642 cache.upsert_with_append("v", key, patch, &[], origin).await
2643 }
2644
2645 #[tokio::test]
2648 async fn an_unmarked_patch_for_a_key_not_held_is_refused_however_old() {
2649 let cache = EntityCache::with_config(EntityCacheConfig {
2650 max_entities_per_view: 1,
2651 ..Default::default()
2652 });
2653 let evictions = EVICTED_KEYS_PER_CACHED_ENTITY * 4;
2654 for key in 0..=evictions {
2655 assert_eq!(
2656 write(
2657 &cache,
2658 &key.to_string(),
2659 json!({"id": key}),
2660 PatchOrigin::Creation
2661 )
2662 .await,
2663 CacheWrite::Created,
2664 "a creation is stored as soon as it arrives"
2665 );
2666 }
2667 assert_eq!(
2669 write(&cache, "0", json!({"n": 1}), PatchOrigin::Change).await,
2670 CacheWrite::Refused {
2671 patch: json!({"n": 1})
2672 }
2673 );
2674 assert!(
2675 matches!(
2676 write(&cache, "never", json!({"n": 1}), PatchOrigin::Change).await,
2677 CacheWrite::Refused { .. }
2678 ),
2679 "nor does it matter whether the view ever held the key"
2680 );
2681 assert_eq!(cache.get("v", "0").await, None);
2682 assert_eq!(
2683 write(
2684 &cache,
2685 &evictions.to_string(),
2686 json!({"n": 1}),
2687 PatchOrigin::Change
2688 )
2689 .await,
2690 CacheWrite::Merged
2691 );
2692 assert_eq!(
2694 write(&cache, "0", json!({"id": 0, "n": 2}), PatchOrigin::Creation).await,
2695 CacheWrite::Created
2696 );
2697 assert_eq!(cache.get("v", "0").await, Some(json!({"id": 0, "n": 2})));
2698 }
2699
2700 #[tokio::test]
2703 async fn a_creation_for_a_key_held_replaces() {
2704 let cache = EntityCache::new();
2705 write(
2706 &cache,
2707 "a",
2708 json!({"id": "a", "old": 1}),
2709 PatchOrigin::Creation,
2710 )
2711 .await;
2712 assert_eq!(
2713 write(
2714 &cache,
2715 "a",
2716 json!({"id": "a", "n": 1}),
2717 PatchOrigin::Creation
2718 )
2719 .await,
2720 CacheWrite::Created
2721 );
2722 assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2723 }
2724
2725 async fn remembers_evictions(cache: &EntityCache, view_id: &str) -> bool {
2726 cache
2727 .caches
2728 .read()
2729 .await
2730 .get(view_id)
2731 .is_some_and(|view| view.evicted.is_some())
2732 }
2733
2734 #[tokio::test]
2738 async fn a_view_fed_marked_creations_keeps_no_eviction_memory() {
2739 let cache = EntityCache::with_config(EntityCacheConfig {
2740 max_entities_per_view: 1,
2741 ..Default::default()
2742 });
2743 cache.remember_evicted("v", ["restored".to_string()]).await;
2744 assert!(remembers_evictions(&cache, "v").await);
2745
2746 write(&cache, "a", json!({"id": "a"}), PatchOrigin::Creation).await;
2747 assert!(!remembers_evictions(&cache, "v").await);
2748 write(&cache, "b", json!({"id": "b"}), PatchOrigin::Creation).await;
2749 assert!(!remembers_evictions(&cache, "v").await, "a was evicted");
2750 assert!(matches!(
2751 write(&cache, "restored", json!({"n": 1}), PatchOrigin::Change).await,
2752 CacheWrite::Refused { .. }
2753 ));
2754 assert!(matches!(
2755 write(&cache, "a", json!({"n": 1}), PatchOrigin::Change).await,
2756 CacheWrite::Refused { .. }
2757 ));
2758 }
2759
2760 #[tokio::test]
2761 async fn remembered_keys_are_refused_unless_held() {
2762 let cache = EntityCache::new();
2763 cache.upsert("tokens/list", "held", json!({"id": 1})).await;
2764 cache
2765 .remember_evicted("tokens/list", ["held".to_string(), "gone".to_string()])
2766 .await;
2767 assert_eq!(
2768 cache.upsert("tokens/list", "held", json!({"n": 1})).await,
2769 CacheWrite::Merged
2770 );
2771 assert!(matches!(
2772 cache.upsert("tokens/list", "gone", json!({"n": 1})).await,
2773 CacheWrite::Refused { .. }
2774 ));
2775 assert_eq!(
2777 cache.upsert("tokens/state", "gone", json!({"id": 2})).await,
2778 CacheWrite::Created
2779 );
2780 }
2781
2782 #[tokio::test]
2783 async fn test_get_all() {
2784 let cache = EntityCache::new();
2785
2786 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2787 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2788
2789 let all = cache.get_all("tokens/list").await;
2790 assert_eq!(all.len(), 2);
2791 }
2792
2793 #[tokio::test]
2794 async fn remove_is_scoped_to_one_entity() {
2795 let cache = EntityCache::new();
2796 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
2797 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
2798
2799 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
2800 assert!(cache.get("tokens/list", "one").await.is_none());
2801 assert!(cache.get("tokens/list", "two").await.is_some());
2802 }
2803
2804 #[tokio::test]
2805 async fn test_separate_views() {
2806 let cache = EntityCache::new();
2807
2808 cache
2809 .upsert("tokens/list", "key1", json!({"type": "token"}))
2810 .await;
2811 cache
2812 .upsert("games/list", "key1", json!({"type": "game"}))
2813 .await;
2814
2815 let token = cache.get("tokens/list", "key1").await.unwrap();
2816 let game = cache.get("games/list", "key1").await.unwrap();
2817
2818 assert_eq!(token["type"], "token");
2819 assert_eq!(game["type"], "game");
2820 }
2821
2822 #[test]
2823 fn test_deep_merge_with_append() {
2824 let mut base = json!({
2825 "a": 1,
2826 "b": {"c": 2},
2827 "arr": [1, 2]
2828 });
2829
2830 let patch = json!({
2831 "b": {"d": 3},
2832 "arr": [3],
2833 "e": 4
2834 });
2835
2836 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
2837
2838 assert_eq!(base["a"], 1);
2839 assert_eq!(base["b"]["c"], 2);
2840 assert_eq!(base["b"]["d"], 3);
2841 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
2842 assert_eq!(base["e"], 4);
2843 }
2844
2845 #[test]
2846 fn test_deep_merge_replace_array() {
2847 let mut base = json!({
2848 "arr": [1, 2, 3]
2849 });
2850
2851 let patch = json!({
2852 "arr": [4, 5]
2853 });
2854
2855 deep_merge_with_append(&mut base, patch, &[], 100);
2856
2857 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
2858 assert_eq!(base["arr"][0], 4);
2859 assert_eq!(base["arr"][1], 5);
2860 }
2861
2862 #[test]
2863 fn test_deep_merge_nested_append() {
2864 let mut base = json!({
2865 "stats": {"events": [1, 2]}
2866 });
2867
2868 let patch = json!({
2869 "stats": {"events": [3]}
2870 });
2871
2872 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
2873
2874 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
2875 }
2876
2877 #[test]
2878 fn test_snapshot_config_defaults() {
2879 let cache = EntityCache::new();
2880 let config = cache.snapshot_config();
2881
2882 assert_eq!(config.initial_batch_size, 50);
2883 assert_eq!(config.subsequent_batch_size, 100);
2884 }
2885
2886 #[test]
2887 fn test_snapshot_config_custom() {
2888 let config = EntityCacheConfig {
2889 initial_snapshot_batch_size: 25,
2890 subsequent_snapshot_batch_size: 75,
2891 ..Default::default()
2892 };
2893 let cache = EntityCache::with_config(config);
2894 let snapshot_config = cache.snapshot_config();
2895
2896 assert_eq!(snapshot_config.initial_batch_size, 25);
2897 assert_eq!(snapshot_config.subsequent_batch_size, 75);
2898 }
2899
2900 #[tokio::test]
2901 async fn test_get_after() {
2902 let cache = EntityCache::new();
2903
2904 cache
2906 .upsert(
2907 "tokens/list",
2908 "key1",
2909 json!({"id": 1, "_seq": "100:000000000001"}),
2910 )
2911 .await;
2912 cache
2913 .upsert(
2914 "tokens/list",
2915 "key2",
2916 json!({"id": 2, "_seq": "100:000000000002"}),
2917 )
2918 .await;
2919 cache
2920 .upsert(
2921 "tokens/list",
2922 "key3",
2923 json!({"id": 3, "_seq": "100:000000000003"}),
2924 )
2925 .await;
2926 cache
2927 .upsert(
2928 "tokens/list",
2929 "key4",
2930 json!({"id": 4, "_seq": "101:000000000001"}),
2931 )
2932 .await;
2933
2934 let after = cache
2936 .get_after("tokens/list", "100:000000000002", None)
2937 .await;
2938
2939 assert_eq!(after.len(), 2);
2941 assert_eq!(after[0].0, "key3");
2942 assert_eq!(after[1].0, "key4");
2943 }
2944
2945 #[tokio::test]
2946 async fn test_get_after_with_limit() {
2947 let cache = EntityCache::new();
2948
2949 cache
2951 .upsert(
2952 "tokens/list",
2953 "key1",
2954 json!({"id": 1, "_seq": "100:000000000001"}),
2955 )
2956 .await;
2957 cache
2958 .upsert(
2959 "tokens/list",
2960 "key2",
2961 json!({"id": 2, "_seq": "100:000000000002"}),
2962 )
2963 .await;
2964 cache
2965 .upsert(
2966 "tokens/list",
2967 "key3",
2968 json!({"id": 3, "_seq": "100:000000000003"}),
2969 )
2970 .await;
2971
2972 let after = cache
2974 .get_after("tokens/list", "100:000000000000", Some(2))
2975 .await;
2976
2977 assert_eq!(after.len(), 2);
2979 assert_eq!(after[0].0, "key1");
2980 assert_eq!(after[1].0, "key2");
2981 }
2982
2983 #[tokio::test]
2984 async fn test_get_after_empty_result() {
2985 let cache = EntityCache::new();
2986
2987 cache
2988 .upsert(
2989 "tokens/list",
2990 "key1",
2991 json!({"id": 1, "_seq": "100:000000000001"}),
2992 )
2993 .await;
2994
2995 let after = cache
2997 .get_after("tokens/list", "999:000000000000", None)
2998 .await;
2999
3000 assert!(after.is_empty());
3001 }
3002
3003 #[tokio::test]
3004 async fn test_get_after_missing_seq() {
3005 let cache = EntityCache::new();
3006
3007 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
3009
3010 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
3012
3013 assert!(after.is_empty());
3014 }
3015
3016 fn versions<'a>(views: &[(&'a str, &str)]) -> Vec<ViewVersion<'a>> {
3017 views
3018 .iter()
3019 .map(|(view_id, version)| ViewVersion {
3020 view_id,
3021 version: Some(json!(version)),
3022 })
3023 .collect()
3024 }
3025
3026 const VIEWS: [&str; 3] = ["t/list", "t/state", "t/append"];
3027
3028 async fn shared(cache: &EntityCache, key: &str) -> Vec<SharedEntity> {
3029 let mut entities = Vec::new();
3030 for view in VIEWS {
3031 entities.push(cache.get_shared(view, key).await.unwrap());
3032 }
3033 entities
3034 }
3035
3036 async fn write_all(
3037 cache: &EntityCache,
3038 key: &str,
3039 patch: Value,
3040 stamps: [&str; 3],
3041 origin: PatchOrigin,
3042 ) -> Vec<CacheWrite> {
3043 let views: Vec<_> = VIEWS.into_iter().zip(stamps).collect();
3044 cache
3045 .upsert_views(
3046 key,
3047 patch,
3048 &versions(&views),
3049 &["events".to_string()],
3050 origin,
3051 LifetimeOrdering::default(),
3052 )
3053 .await
3054 }
3055
3056 #[tokio::test]
3060 async fn views_written_together_share_one_copy() {
3061 let cache = EntityCache::new();
3062 let writes = write_all(
3063 &cache,
3064 "a",
3065 json!({"id": "a", "events": [1]}),
3066 ["e:1", "e:2", "e:3"],
3067 PatchOrigin::Creation,
3068 )
3069 .await;
3070 assert_eq!(writes, vec![CacheWrite::Created; 3]);
3071 let [list, state, append] =
3072 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
3073 assert!(list.shares_fields_with(&state) && list.shares_fields_with(&append));
3074 assert_eq!(Arc::strong_count(list.fields()), 6);
3076 assert_eq!(
3077 cache.get("t/state", "a").await,
3078 Some(json!({"id": "a", "events": [1], "_version": "e:2"}))
3079 );
3080
3081 let writes = write_all(
3082 &cache,
3083 "a",
3084 json!({"n": 1, "events": [2]}),
3085 ["e:4", "e:5", "e:6"],
3086 PatchOrigin::Unknown,
3087 )
3088 .await;
3089 assert_eq!(writes, vec![CacheWrite::Merged; 3]);
3090 let merged = shared(&cache, "a").await;
3091 assert!(
3092 merged[0].shares_fields_with(&merged[1]) && merged[0].shares_fields_with(&merged[2])
3093 );
3094 for (view, version) in VIEWS.into_iter().zip(["e:4", "e:5", "e:6"]) {
3095 assert_eq!(
3096 cache.get(view, "a").await,
3097 Some(json!({"id": "a", "n": 1, "events": [1, 2], "_version": version})),
3098 "{view}"
3099 );
3100 }
3101 assert_eq!(
3103 list.to_value(),
3104 json!({"id": "a", "events": [1], "_version": "e:1"})
3105 );
3106 assert!(!list.shares_fields_with(&merged[0]));
3107 }
3108
3109 #[tokio::test]
3112 async fn a_merge_nobody_else_holds_happens_in_place() {
3113 let cache = EntityCache::new();
3114 write_all(
3115 &cache,
3116 "a",
3117 json!({"id": "a"}),
3118 ["e:1", "e:2", "e:3"],
3119 PatchOrigin::Creation,
3120 )
3121 .await;
3122 let before = Arc::as_ptr(cache.get_shared("t/list", "a").await.unwrap().fields());
3123 write_all(
3124 &cache,
3125 "a",
3126 json!({"n": 1}),
3127 ["e:4", "e:5", "e:6"],
3128 PatchOrigin::Unknown,
3129 )
3130 .await;
3131 let after = cache.get_shared("t/list", "a").await.unwrap();
3132 assert_eq!(Arc::as_ptr(after.fields()), before);
3135 assert_eq!(Arc::strong_count(after.fields()), 4);
3136 assert_eq!(after["n"], 1);
3137 }
3138
3139 #[tokio::test]
3142 async fn a_patch_to_one_view_does_not_reach_another() {
3143 let cache = EntityCache::new();
3144 write_all(
3145 &cache,
3146 "a",
3147 json!({"id": "a", "nested": {"n": 0}}),
3148 ["e:1", "e:2", "e:3"],
3149 PatchOrigin::Creation,
3150 )
3151 .await;
3152 assert_eq!(
3153 cache
3154 .upsert_with_append(
3155 "t/list",
3156 "a",
3157 json!({"nested": {"n": 1}, "_version": "e:4"}),
3158 &[],
3159 PatchOrigin::Unknown,
3160 )
3161 .await,
3162 CacheWrite::Merged
3163 );
3164 assert_eq!(
3165 cache.get("t/list", "a").await,
3166 Some(json!({"id": "a", "nested": {"n": 1}, "_version": "e:4"}))
3167 );
3168 for (view, version) in [("t/state", "e:2"), ("t/append", "e:3")] {
3169 assert_eq!(
3170 cache.get(view, "a").await,
3171 Some(json!({"id": "a", "nested": {"n": 0}, "_version": version})),
3172 "{view}"
3173 );
3174 }
3175 let [list, state, append] =
3176 <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
3177 assert!(!list.shares_fields_with(&state));
3178 assert!(state.shares_fields_with(&append));
3179 }
3180
3181 #[tokio::test]
3184 async fn a_view_that_refuses_a_patch_keeps_its_copy() {
3185 let cache = EntityCache::new();
3186 write_all(
3187 &cache,
3188 "a",
3189 json!({"id": "a", "n": 0}),
3190 ["e:1", "e:2", "e:3"],
3191 PatchOrigin::Creation,
3192 )
3193 .await;
3194 assert!(cache.delete_current("t/state", "a").await);
3196 let writes = write_all(
3197 &cache,
3198 "a",
3199 json!({"n": 1}),
3200 ["e:4", "e:5", "e:6"],
3201 PatchOrigin::Change,
3202 )
3203 .await;
3204 assert_eq!(writes[0], CacheWrite::Merged);
3205 assert_eq!(
3206 writes[1],
3207 CacheWrite::Refused {
3208 patch: json!({"n": 1, "_version": "e:5"})
3209 }
3210 );
3211 assert_eq!(writes[2], CacheWrite::Merged);
3212 assert_eq!(cache.get("t/state", "a").await, None);
3213 let list = cache.get_shared("t/list", "a").await.unwrap();
3214 let append = cache.get_shared("t/append", "a").await.unwrap();
3215 assert!(list.shares_fields_with(&append));
3216 assert_eq!(
3217 append.to_value(),
3218 json!({"id": "a", "n": 1, "_version": "e:6"})
3219 );
3220 }
3221
3222 #[tokio::test]
3225 async fn a_restore_shares_the_copies_it_saved_per_view() {
3226 let cache = EntityCache::new();
3227 write_all(
3228 &cache,
3229 "a",
3230 json!({"id": "a", "events": [1, 2]}),
3231 ["e:1", "e:2", "e:3"],
3232 PatchOrigin::Creation,
3233 )
3234 .await;
3235 let mut dump = cache.dump().await;
3236 dump.push((
3239 "other/list".to_string(),
3240 vec![("a".to_string(), json!({"id": "a", "events": [1, 2]}))],
3241 ));
3242 dump.sort_by(|left, right| left.0.cmp(&right.0));
3243
3244 let restored = EntityCache::new();
3245 restored.hydrate(dump.clone()).await;
3246 let other = restored.get_shared("other/list", "a").await.unwrap();
3247 let copies = shared(&restored, "a").await;
3248 assert!(!other.shares_fields_with(&copies[0]));
3249 assert!(
3250 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3251 );
3252 let mut again = restored.dump().await;
3253 again.sort_by(|left, right| left.0.cmp(&right.0));
3254 assert_eq!(again, dump);
3255 }
3256
3257 #[tokio::test]
3259 async fn a_whole_entity_stored_in_several_views_is_one_copy() {
3260 let cache = EntityCache::new();
3261 let views = versions(&[("t/list", "e:1"), ("t/state", "e:2"), ("t/append", "e:3")]);
3262 let stored = cache
3263 .store_whole_views("a", json!({"id": "a"}), &views, None, None)
3264 .await;
3265 assert_eq!(stored, vec![true; 3]);
3266 let copies = shared(&cache, "a").await;
3267 assert!(
3268 copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3269 );
3270 assert_eq!(
3271 cache.get("t/append", "a").await,
3272 Some(json!({"id": "a", "_version": "e:3"}))
3273 );
3274 }
3275
3276 fn member<'a>(entity: &'a SharedEntity, name: &str) -> &'a Arc<Value> {
3278 match &**entity.fields() {
3279 Fields::Object(members) => members.get(name).expect("the field is there"),
3280 Fields::Other(other) => panic!("not an object: {other}"),
3281 }
3282 }
3283
3284 #[tokio::test]
3287 async fn a_patch_copies_only_the_sections_it_changes() {
3288 let cache = EntityCache::new();
3289 let row = json!({
3290 "id": {"address": "a", "mint": "m"},
3291 "balance": {"amount": "1", "state": "initialized"},
3292 "activity": {"transfers": 0},
3293 });
3294 cache.upsert("t/list", "a", row.clone()).await;
3295 let held = cache.get_shared("t/list", "a").await.unwrap();
3297
3298 cache
3299 .upsert("t/list", "a", json!({"balance": {"amount": "2"}}))
3300 .await;
3301 let after = cache.get_shared("t/list", "a").await.unwrap();
3302 assert!(!after.shares_fields_with(&held));
3303 assert!(Arc::ptr_eq(member(&after, "id"), member(&held, "id")));
3304 assert!(Arc::ptr_eq(
3305 member(&after, "activity"),
3306 member(&held, "activity")
3307 ));
3308 assert!(!Arc::ptr_eq(
3309 member(&after, "balance"),
3310 member(&held, "balance")
3311 ));
3312 assert_eq!(held.to_value(), row, "the other holder's copy is as it was");
3313 assert_eq!(
3314 after.to_value(),
3315 json!({
3316 "id": {"address": "a", "mint": "m"},
3317 "balance": {"amount": "2", "state": "initialized"},
3318 "activity": {"transfers": 0},
3319 })
3320 );
3321
3322 let held = after;
3325 let mut resent = held.to_value();
3326 resent["activity"]["transfers"] = json!(1);
3327 cache.upsert("t/list", "a", resent.clone()).await;
3328 let after = cache.get_shared("t/list", "a").await.unwrap();
3329 assert!(Arc::ptr_eq(member(&after, "id"), member(&held, "id")));
3330 assert!(Arc::ptr_eq(
3331 member(&after, "balance"),
3332 member(&held, "balance")
3333 ));
3334 assert!(!Arc::ptr_eq(
3335 member(&after, "activity"),
3336 member(&held, "activity")
3337 ));
3338 assert_eq!(after.to_value(), resent);
3339 }
3340
3341 #[test]
3345 fn a_section_merge_matches_the_whole_value_merge() {
3346 let base = json!({
3347 "id": {"address": "a", "tags": ["x", "y"]},
3348 "balance": {"amount": "1", "nested": {"deep": [1, 2], "n": null}},
3349 "events": [1, 2, 3],
3350 "list": [1],
3351 "scalar": 5,
3352 });
3353 let patches = [
3354 json!({}),
3355 json!({"id": {"address": "a", "tags": ["x", "y"]}}),
3356 json!({"balance": {"amount": "2"}}),
3357 json!({"balance": {"nested": {"deep": [3], "extra": {"arr": [1, 2, 3, 4]}}}}),
3358 json!({"balance": {"nested": {"deep": [1, 2, 3, 4]}}}),
3359 json!({"events": [4]}),
3360 json!({"events": []}),
3361 json!({"events": [4, 5, 6, 7, 8]}),
3362 json!({"list": [2, 3, 4, 5]}),
3363 json!({"list": [1]}),
3364 json!({"scalar": {"now": "an object", "arr": [1, 2, 3, 4]}}),
3365 json!({"scalar": 5, "new": [1, 2, 3, 4, 5], "id": null}),
3366 json!({"balance": [1, 2, 3, 4], "events": {"replaced": true}}),
3367 json!({"balance": {"nested": {"n": null}}}),
3368 json!({"balance": {"amount": 1}}),
3369 json!({"id": {"tags": ["x", "y", "z", "w"]}}),
3370 ];
3371 let append = ["events".to_string(), "balance.nested.deep".to_string()];
3372 for max_array_length in [3, 100] {
3373 for patch in &patches {
3374 let mut expected = base.clone();
3375 deep_merge_with_append(&mut expected, patch.clone(), &append, max_array_length);
3376 for shared in [false, true] {
3377 let mut entity = SharedEntity::new(base.clone());
3378 let other = shared.then(|| entity.clone());
3379 merge_shared(&mut entity, patch.clone(), &append, max_array_length);
3380 assert_eq!(
3381 serde_json::to_string(&entity).unwrap(),
3382 serde_json::to_string(&expected).unwrap(),
3383 "{patch} (shared: {shared}, max {max_array_length})"
3384 );
3385 if let Some(other) = other {
3386 assert_eq!(other.to_value(), base, "the other holder's copy");
3387 }
3388 }
3389 }
3390 }
3391 }
3392
3393 #[test]
3396 fn a_shared_merge_matches_the_whole_value_merge() {
3397 let cases = [
3398 (
3399 json!({"a": 1, "_version": "e:1"}),
3400 json!({"b": 2, "_version": "e:2"}),
3401 ),
3402 (json!({"a": 1, "_version": "e:1"}), json!({"b": 2})),
3403 (json!({"a": 1, "_version": "e:1"}), json!([1, 2])),
3404 (json!([1, 2]), json!([3])),
3405 (json!([1, 2]), json!({"a": 1, "_version": "e:2"})),
3406 (json!("text"), json!(5)),
3407 (
3408 json!({"_version": "e:1", "list": [1]}),
3409 json!({"list": [2, 3, 4]}),
3410 ),
3411 ];
3412 for (base, patch) in cases {
3413 let append = ["list".to_string()];
3414 let mut expected = base.clone();
3415 deep_merge_with_append(&mut expected, patch.clone(), &append, 2);
3416 let mut entity = SharedEntity::new(base.clone());
3417 let other = entity.clone();
3418 merge_shared(&mut entity, patch.clone(), &append, 2);
3419 assert_eq!(entity.to_value(), expected, "{base} <- {patch}");
3420 assert_eq!(other.to_value(), base, "the other holder's copy");
3421 }
3422 }
3423}