Skip to main content

arete_server/
cache.rs

1//! Entity cache for snapshot-on-subscribe functionality.
2//!
3//! This module provides an `EntityCache` that maintains full projected entities
4//! in memory with LRU eviction. When a new client subscribes, they receive
5//! cached snapshots immediately rather than waiting for the next live mutation.
6
7use 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;
24/// Evicted keys remembered per cached entity, for a source that does not mark
25/// creations (see [`PatchOrigin::Unknown`]). Such a source can keep far more
26/// entities than a view caches (a VM keeps 2,500 per state table by default
27/// against 500 here), so a key the cache evicted can keep receiving patches
28/// long afterwards; eight times the cache bound covers the VM's default table
29/// with room to spare. A key evicted longer ago than that is taken for new.
30///
31/// A source that marks creations needs no memory: an unmarked patch for a key
32/// a view does not hold is refused however long ago the key left, or whether
33/// it was ever there.
34const EVICTED_KEYS_PER_CACHED_ENTITY: usize = 8;
35
36/// Whether a patch creates its entity, as far as its source says.
37///
38/// Every patch carries only the fields that changed. For a key a view holds
39/// that is all it needs; for one it does not hold, the patch is the entity
40/// only if it created it.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum PatchOrigin {
43    /// The source marked the patch as its entity's creation
44    /// ([`arete_interpreter::WholeEntity::Created`]), so it is all of the
45    /// entity.
46    Creation,
47    /// The source marks every creation and did not mark this patch, so it
48    /// changes an entity created earlier and is only part of it. A VM linked
49    /// through [`crate::EntityResync`] is such a source.
50    Change,
51    /// The source does not mark creations. A patch for a key the view does not
52    /// hold is taken for a new entity unless the view remembers evicting it.
53    Unknown,
54}
55
56/// What [`EntityCache::upsert_with_append`] did with a patch.
57#[derive(Debug, Clone, PartialEq)]
58pub enum CacheWrite {
59    /// Merged into the entity the cache already held.
60    Merged,
61    /// Stored as a new entity: the patch created it, or its source does not
62    /// say and the cache has no record of the key.
63    Created,
64    /// Not stored. The cache does not hold the key and the patch carries only
65    /// the fields that changed — part of an entity, not one: its source marks
66    /// creations and did not mark this one, or the cache evicted the key. The
67    /// patch is handed back for callers that hold a full copy elsewhere.
68    Refused { patch: Value },
69}
70
71/// What one view does with a patch for a key, decided before any view
72/// changes (see [`EntityCache::upsert_with_ordering`]).
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74enum Admission {
75    Refuse,
76    Create,
77    Merge,
78}
79
80/// One view's part in a write to several views at once: the view, and the
81/// `_version` its frame carries.
82#[derive(Debug, Clone)]
83pub(crate) struct ViewVersion<'a> {
84    pub view_id: &'a str,
85    pub version: Option<Value>,
86}
87
88/// One view's cached entities and the keys it evicted to stay bounded.
89///
90/// Both maps are unbounded `LruCache`s capped by hand: a bounded one
91/// allocates its full capacity up front, which for the evicted-key memory is
92/// several times the entity bound, per view, whether or not anything is ever
93/// evicted from it.
94struct ViewEntries {
95    max_entities: usize,
96    entities: LruCache<String, SharedEntity>,
97    /// Keys evicted for space, most recently evicted or refused first.
98    /// Allocated on the first eviction, and never for a view fed by a source
99    /// that marks creations.
100    evicted: Option<LruCache<String, ()>>,
101    /// Whether a source that marks creations writes to this view. Such a
102    /// source never consults `evicted`.
103    creations_marked: bool,
104    // Recent authoritative account positions and deletion barriers survive
105    // snapshots. They are intentionally separate from entity `_seq`, whose
106    // offset may instead be an instruction txn_index.
107    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    /// Note a patch from a source that marks creations: this view no longer
123    /// needs to remember its evictions.
124    fn mark_creations(&mut self) {
125        self.creations_marked = true;
126        self.evicted = None;
127    }
128
129    /// Store `entity` under `key`, remembering whatever it pushes out.
130    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    /// Whether `key` was evicted and is still remembered. Asking refreshes
162    /// it, so an evicted key that keeps changing is not forgotten.
163    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                // Once a producer opts into authoritative account ordering it
181                // must carry it on every later account-owned lifetime change.
182                (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        // An account cursor and an instruction/source cursor are independent.
203        // In particular, do not recover the account update's `_seq` from the
204        // entity and compare its write version with an instruction index.
205        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        // Only the legacy domain may recover ordering from an entity written
210        // before explicit source domains were recorded. An entity `_seq`
211        // alone cannot say whether its offset is an account write version,
212        // instruction index or resolver counter.
213        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            // Once a markerless source supplies ordering, an unsequenced
241            // lifetime change cannot prove that it is newer.
242            (Some(_), None) => false,
243            (None, _) => true,
244        }
245    }
246
247    /// Check a patch for `key` against the key's lifetime ordering, record
248    /// its position, and decide what it does to this view: refused, stored as
249    /// a new entity, or merged into the one held (see [`EntityCache`]'s "Only
250    /// whole entities").
251    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            // Account writes choose the lifetime, but they do not reset the
273            // independent instruction/resolver high-water marks. Keeping
274            // those cursors across deletion and recreation prevents a late
275            // duplicate from the old lifetime from contaminating the new
276            // row. A genuinely newer source event still advances its own
277            // domain normally.
278            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            // An entirely unsequenced source preserves its historical
324            // recreate behavior; there is no ordering evidence to retain.
325            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        // A creation is a complete replacement. Deep-merging it would retain
337        // fields from the previous account lifetime.
338        if creation || !self.entities.contains(key) {
339            Admission::Create
340        } else {
341            Admission::Merge
342        }
343    }
344
345    /// Whether a whole entity requested for `key` may be stored: it was
346    /// requested for the account lifetime and source position that are still
347    /// current (see [`EntityCache::store_whole_for_lifetime`]).
348    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/// The authoritative account lifetime known for one projected entity.
383#[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    /// Ordering for producers that do not carry an authoritative account
389    /// position. This is never compared with `account_position`.
390    #[serde(skip_serializing_if = "Option::is_none")]
391    pub source_seq: Option<String>,
392    /// Explicit producer-local cursors. Each domain is compared only with
393    /// itself, so resolver counters cannot suppress later instructions.
394    #[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
412/// `(view, key, checkpoint)` in most-recently-changed order.
413pub type EntityLifetimes = Vec<(String, String, EntityLifetimeCheckpoint)>;
414
415/// Independent ordering metadata attached to one projected cache write.
416#[derive(Debug, Clone, Copy, Default)]
417pub struct LifetimeOrdering<'a> {
418    pub account_position: Option<AccountPosition>,
419    pub source_seq: Option<&'a str>,
420    /// The producer-local domain in which `source_seq` is comparable.
421    /// `None` deliberately disables source recency checks.
422    pub source_domain: Option<SlotIndexDomain>,
423}
424
425/// Legacy `(view, key, _seq)` barriers, retained only to restore snapshots
426/// written before account positions had their own ordering domain.
427pub type EntityTombstones = Vec<(String, String, Option<String>)>;
428
429/// Compare two `_seq` values numerically.
430/// `_seq` format is "{slot}:{offset}" where slot is not zero-padded.
431/// This handles digit-count boundaries correctly (e.g., 99999999 < 100000000).
432pub 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/// Configuration for the entity cache
451#[derive(Debug, Clone)]
452pub struct EntityCacheConfig {
453    /// Maximum number of entities to cache per view
454    pub max_entities_per_view: usize,
455    /// Maximum array length before oldest elements are evicted
456    pub max_array_length: usize,
457    /// Number of entities to send in the first snapshot batch (for fast initial render)
458    pub initial_snapshot_batch_size: usize,
459    /// Number of entities to send in subsequent snapshot batches
460    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/// Entity cache that maintains full projected entities with LRU eviction.
475///
476/// The cache is populated as mutations flow through the projector, regardless
477/// of subscriber state. When a new subscriber connects, they receive snapshots
478/// of all cached entities for their requested view.
479///
480/// # Only whole entities
481///
482/// Source mutations carry only the fields that changed. A patch for a key a
483/// view holds merges into the whole entity. A patch for a key it does not
484/// hold is stored only if it is the whole entity: storing a few changed
485/// fields would serve them as though they were the entity, in snapshots and
486/// in every derived view.
487///
488/// A VM linked through [`crate::EntityResync`] marks the mutation that creates
489/// each entity ([`PatchOrigin::Creation`]), which is stored as-is. Any other
490/// patch from it for a key a view does not hold ([`PatchOrigin::Change`]) is
491/// refused with [`CacheWrite::Refused`] — whether the view evicted the key or
492/// never held it, as after a restore. The projector then asks the VM for the
493/// whole entity, which follows in the VM's next batch and is stored with
494/// [`EntityCache::store_whole`]. That holds however many entities the VM
495/// keeps and however long ago the view evicted the key.
496///
497/// A source that marks nothing ([`PatchOrigin::Unknown`]) leaves a new key and
498/// an evicted one looking alike. Each view therefore remembers the keys it
499/// evicted (bounded, most recent first) and refuses patches for them; a key
500/// evicted so long ago that it has been forgotten is taken for new. A source
501/// delete also ends an eviction.
502#[derive(Clone)]
503pub struct EntityCache {
504    /// view_id -> LRU<entity_key, full_projected_entity>, plus evicted keys
505    caches: Arc<RwLock<HashMap<String, ViewEntries>>>,
506    config: EntityCacheConfig,
507}
508
509impl EntityCache {
510    /// Create a new entity cache with default configuration
511    pub fn new() -> Self {
512        Self::with_config(EntityCacheConfig::default())
513    }
514
515    /// Create a new entity cache with custom configuration
516    pub fn with_config(config: EntityCacheConfig) -> Self {
517        Self {
518            caches: Arc::new(RwLock::new(HashMap::new())),
519            config,
520        }
521    }
522
523    /// Maximum number of entities cached per view.
524    ///
525    /// Derived sorted view caches are bounded by the same value so they never
526    /// hold more entities than their source view caches.
527    pub fn max_entities_per_view(&self) -> usize {
528        self.config.max_entities_per_view
529    }
530
531    /// Write a patch from a source that does not mark creations
532    /// ([`PatchOrigin::Unknown`]).
533    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    /// Merge `patch` into the cached entity, store it as a new entity, or
539    /// refuse it as part of an entity the cache does not hold (see the type
540    /// docs).
541    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    /// Apply a projected patch with optional authoritative account ordering.
554    /// `_seq` is consulted for lifetime changes only when no account position
555    /// exists; the two ordering domains are never compared.
556    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                // The convenience path supports markerless lifetime ordering,
576                // but sparse legacy patches may mix account write versions
577                // and instruction indices within one slot.
578                source_domain: (origin == PatchOrigin::Creation).then_some(SlotIndexDomain::Legacy),
579            },
580        )
581        .await
582    }
583
584    /// Apply a projected patch with both ordering domains supplied
585    /// explicitly. Projections may omit `_seq`, so the projector passes its
586    /// source cursor separately here.
587    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    /// Apply one patch to several views of its export at once, each with the
611    /// `_version` its own frame carries. `patch` holds every other field.
612    ///
613    /// Each view decides what to do with the patch exactly as
614    /// [`Self::upsert_with_ordering`] would on its own. Views that store it
615    /// whole share one copy of it, and views that hold the same fields (see
616    /// [`SharedEntity`]) merge it into them once and keep sharing the result.
617    /// That merge happens in place unless something outside these views
618    /// (a derived view, a subscriber) still holds the fields, in which case
619    /// the merge copies only the top-level fields it changes and the other
620    /// holder keeps the old ones, sharing the rest with the new.
621    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        // A stray `_version` in the patch stands for any view without one,
631        // and must not end up among the shared fields.
632        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        // Take the fields out of every merging view, so views that share them
662        // can merge them once: in place when no other holder is left.
663        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                // Admitted under this lock as held, so this cannot happen;
674                // storing the patch whole is what a view without it does.
675                *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                    // Not two objects: the patch replaces the entity, or
749                    // merges into the array it is. Rare enough to do per view.
750                    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    /// Store `entity` as the whole entity for `key`, replacing anything held
763    /// and clearing an eviction: the source vouched for it being complete
764    /// (see [`arete_interpreter::Mutation::mark_whole_entity`]).
765    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    /// Store a requested whole entity only if the request was made for the
771    /// account lifetime that is still current.
772    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    /// Store a whole entity in several views at once, each under its own
843    /// `_version`, where the checks of [`Self::store_whole_for_lifetime`]
844    /// (or, with `requested_source`,
845    /// [`Self::store_whole_for_ordering_in_domain`]) pass. `entity` holds every
846    /// other field; the views that store it share one copy.
847    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    /// Apply an ordered source deletion. A delayed deletion never removes a
957    /// newer recreation. Only a newer, explicitly marked creation can restart
958    /// a deleted row; sparse patches and resends cannot resurrect it.
959    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    /// Delete the lifetime currently held by the cache when the producer has
975    /// no cursor to attach. This is an explicit, trusted operation: callers
976    /// with ordering evidence must use [`Self::delete_ordered`] instead.
977    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    /// Delete using the producer's own `_seq` ordering only when no
1009    /// authoritative account position is present.
1010    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    /// Restore the old `_seq` barriers as account positions. Those barriers
1116    /// were written only by authoritative account deletion/recreation paths;
1117    /// whether the row exists distinguishes a live recreation from deletion.
1118    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    /// Whether the shared cache says a published delete still represents the
1140    /// latest lifetime. Subscription tasks use this without mutating the cache
1141    /// a second time after the projector has already applied the delete.
1142    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    /// Merge a source patch into `base` with this cache's append and array
1154    /// rules, for callers that keep their own full copy of an entity.
1155    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    /// [`Self::merge_patch`] for a [`SharedEntity`]: copies its fields first
1160    /// if another holder shares them, so the merge never reaches that holder.
1161    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    /// Treat `keys` as evicted from `view_id` unless the view holds them, so
1171    /// patches for them from a source that does not mark creations are
1172    /// refused. Used after a restore, when the VM holds entities the restored
1173    /// cache does not; a VM that marks creations makes this moot, and the
1174    /// view drops the memory with its first patch.
1175    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    /// Get all cached entities for a view.
1188    ///
1189    /// Returns a vector of (key, entity) pairs for sending as snapshots
1190    /// to new subscribers. Each is a copy; [`Self::get_all_shared`] returns
1191    /// the cached entities themselves.
1192    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    /// Every entity cached for a view, most recently used first, sharing
1201    /// their fields with the cache rather than copying them.
1202    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    /// Get entities with _seq greater than the provided cursor.
1217    ///
1218    /// Returns entities that have been updated after the given cursor,
1219    /// sorted by _seq in ascending order. Useful for resuming from
1220    /// a specific point in the stream.
1221    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            // Sort by _seq (ascending)
1244            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            // Apply limit if provided
1251            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    /// Get a copy of a specific entity from the cache.
1265    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    /// A specific cached entity, sharing its fields with the cache rather
1272    /// than copying them.
1273    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    /// Remove one entity after a source-wide delete.
1281    ///
1282    /// The delete ends the entity, so the key is also forgotten as evicted: a
1283    /// later patch for it creates a new entity.
1284    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    /// Get the number of cached entities for a view
1296    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    /// Check if the cache for a view is empty
1305    pub async fn is_empty(&self, view_id: &str) -> bool {
1306        self.len(view_id).await == 0
1307    }
1308
1309    /// Get the snapshot batch configuration
1310    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    /// Clear all cached entities for a view
1318    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    /// Dump every view's entities for a state snapshot.
1332    ///
1333    /// Entries are ordered most-recently-used first; [`Self::hydrate`] relies
1334    /// on that to reconstruct LRU eviction order. Each is a copy;
1335    /// [`Self::dump_shared`] returns the cached entities themselves.
1336    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    /// [`Self::dump`], sharing the entities' fields with the cache rather
1353    /// than copying them. They serialize exactly as the copies would.
1354    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    /// Restore entities dumped by [`Self::dump`], preserving LRU order.
1371    ///
1372    /// Entities are inserted as-is (no merge): a snapshot holds fully
1373    /// projected entities, not patches. A snapshot saves each view's copy of
1374    /// an entity on its own, so views of one export (`{export}/{mode}`) whose
1375    /// copies have the same fields (all but `_version`) share them again, as
1376    /// they did when saved. Only copies of one export are compared.
1377    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/// A point-in-time count of an [`EntityCache`]'s contents.
1423#[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
1503/// [`deep_merge_with_append`] for an object's shared fields and an object
1504/// patch: the same result, but a top-level field the merge would leave as it
1505/// is keeps sharing its value, and one another holder shares is merged into a
1506/// copy of what it keeps rather than a copy of the whole field.
1507fn 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
1550/// Whether [`deep_merge_with_append_inner`] would leave `base` exactly as it
1551/// is. Conservative: `false` whenever that is not plain.
1552fn 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
1586/// What [`deep_merge_with_append_inner`] makes of a copy of `base`, without
1587/// copying the parts of `base` the patch replaces.
1588fn 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            // The keys `base` has, in its order, then those only the patch
1598            // has, in the patch's: where merging into `base` puts them.
1599            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                // Only the newest `max_array_length` of base and patch stay.
1633                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
1655/// The dotted path of field `key` of the value at `current_path`, which only
1656/// the append check reads: without append paths there is nothing to build.
1657fn 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
1667/// Recursively truncate any arrays in a value to the max length, keeping
1668/// the newest (last) elements.
1669fn 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
1674/// [`truncate_arrays_if_needed`] without rebuilding anything that needs no
1675/// truncation.
1676fn 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
1696/// Merge `patch` into `entity` exactly as [`deep_merge_with_append`] would
1697/// merge it into the whole entity, `_version` included. The fields are copied
1698/// first if another holder shares them.
1699fn 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    // Not two objects: the patch replaces the entity, unless both are arrays
1723    // and it merges into the one held. Only an object has a `_version`, so
1724    // the entity's own goes with it.
1725    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
1742/// The `_version` an entity has after a patch: the patch's, merged into the
1743/// entity's as [`deep_merge_with_append`] merges any top-level field.
1744fn 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        // Neither a stale authoritative write/tombstone nor a delayed resend
1846        // from the deleted lifetime can contaminate the replacement.
1847        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        // Current lifetime metadata survives a snapshot and continues to
1918        // admit instruction-domain offsets while rejecting stale account data.
1919        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        // Duplicates delayed across the deletion/recreation boundary retain
2016        // their old source positions and cannot change the replacement.
2017        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        // A later instruction in the same source domain remains valid even
2051        // though its txn index is lower than account write versions.
2052        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        // An evicted row may be filled by a requested whole-entity resend,
2173        // but a response requested for the previous lifetime must not do it.
2174        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        // [1,2] + [3,4] = [1,2,3,4] → LRU(3) = [2,3,4]
2522        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        // key1 was evicted; this carries only what changed.
2565        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        // A source delete ends the entity: it may then be created again.
2578        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        // Key 0 was pushed out of the memory by the evictions after it; the
2623        // most recent evictions are still remembered.
2624        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    /// A source that marks creations says outright which patches are whole:
2646    /// no memory of evictions is needed, and none can run out.
2647    #[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        // Key 0 was evicted far longer ago than any memory would reach.
2668        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        // The source started key 0 again.
2693        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    /// A creation for a key the view still holds starts a new lifetime and
2701    /// replaces fields that no longer exist.
2702    #[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    /// A view fed by a source that marks creations never consults the
2735    /// evicted-key memory, so it drops what a restore put there and keeps no
2736    /// more.
2737    #[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        // Other views are unaffected.
2776        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        // Insert entities with _seq values
2905        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        // Get all entities after "100:000000000002"
2935        let after = cache
2936            .get_after("tokens/list", "100:000000000002", None)
2937            .await;
2938
2939        // Should return key3 and key4 (sorted by _seq)
2940        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        // Insert entities with _seq values
2950        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        // Get entities after "100:000000000000" with limit 2
2973        let after = cache
2974            .get_after("tokens/list", "100:000000000000", Some(2))
2975            .await;
2976
2977        // Should return only first 2 (key1 and key2)
2978        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        // Get entities after a future cursor
2996        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        // Insert entity without _seq
3008        cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
3009
3010        // Get entities after any cursor - entity without _seq should not be included
3011        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    /// Views written together hold one copy of the entity's fields, each
3057    /// under its own `_version`, and every view still reads as the whole
3058    /// entity its frames describe.
3059    #[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        // Three views and the three copies just read.
3075        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        // The copies read before the merge still hold what they held.
3102        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    /// Nothing else holds the views' shared fields, so the merge writes them
3110    /// in place instead of copying them.
3111    #[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        // Had the merge copied the fields, the copy would have been made
3133        // while the original was still allocated, at another address.
3134        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    /// A patch written to one view leaves another view's copy as it was,
3140    /// even though the two shared their fields until then.
3141    #[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    /// A view that refuses a patch keeps its copy while the views that take
3182    /// it move on to a new one.
3183    #[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        // Only the state view saw the entity's deletion.
3195        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    /// A snapshot saves each view's copy of an entity on its own; restoring
3223    /// it shares the fields again and keeps every view's `_version`.
3224    #[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        // Another export's entity under the same key is not compared, even
3237        // when it happens to have the same fields.
3238        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    /// A whole entity stored in several views at once is one copy.
3258    #[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    /// The value of top-level field `name` of `entity`'s shared fields.
3277    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    /// A patch to one section of an entity another holder still shares
3285    /// copies that section only: the old and new versions share the rest.
3286    #[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        // A derived view or a subscriber holding the current version.
3296        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        // A whole row resent with one section changed keeps sharing the
3323        // sections it repeats.
3324        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    /// Merging into shared fields, held by another holder or not, gives
3342    /// exactly what merging into the whole value gives, and never reaches the
3343    /// other holder's copy.
3344    #[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    /// Merging into a shared copy that is not an object (or with a patch that
3394    /// is not one) still follows the whole-value merge rules.
3395    #[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}