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, 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    /// they are copied first and the other holder keeps the old ones.
620    pub(crate) async fn upsert_views(
621        &self,
622        key: &str,
623        patch: Value,
624        views: &[ViewVersion<'_>],
625        append_paths: &[String],
626        origin: PatchOrigin,
627        ordering: LifetimeOrdering<'_>,
628    ) -> Vec<CacheWrite> {
629        // A stray `_version` in the patch stands for any view without one,
630        // and must not end up among the shared fields.
631        let (mut patch, stray_version) = split_version(patch);
632        let version_of = |index: usize| {
633            views[index]
634                .version
635                .clone()
636                .or_else(|| stray_version.clone())
637        };
638        let max_array_length = self.config.max_array_length;
639        let mut caches = self.caches.write().await;
640
641        let mut admissions: Vec<Admission> = views
642            .iter()
643            .map(|view| {
644                caches
645                    .entry(view.view_id.to_string())
646                    .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view))
647                    .admit(key, origin, ordering)
648            })
649            .collect();
650
651        let mut writes = vec![CacheWrite::Merged; views.len()];
652        for (index, admission) in admissions.iter().enumerate() {
653            if *admission == Admission::Refuse {
654                writes[index] = CacheWrite::Refused {
655                    patch: with_version(patch.clone(), version_of(index)),
656                };
657            }
658        }
659
660        // Take the fields out of every merging view, so views that share them
661        // can merge them once: in place when no other holder is left.
662        let placeholder = Arc::new(Value::Null);
663        let mut groups: Vec<(Arc<Value>, Vec<usize>)> = Vec::new();
664        for (index, admission) in admissions.iter_mut().enumerate() {
665            if *admission != Admission::Merge {
666                continue;
667            }
668            let held = caches
669                .get_mut(views[index].view_id)
670                .and_then(|view| view.entities.get_mut(key));
671            let Some(entity) = held else {
672                // Admitted under this lock as held, so this cannot happen;
673                // storing the patch whole is what a view without it does.
674                *admission = Admission::Create;
675                continue;
676            };
677            let fields = std::mem::replace(entity.fields_mut(), placeholder.clone());
678            match groups
679                .iter_mut()
680                .find(|(shared, _)| Arc::ptr_eq(shared, &fields))
681            {
682                Some((_, members)) => members.push(index),
683                None => groups.push((fields, vec![index])),
684            }
685        }
686
687        if admissions.contains(&Admission::Create) {
688            let source = if groups.is_empty() {
689                std::mem::take(&mut patch)
690            } else {
691                patch.clone()
692            };
693            let created = Arc::new(truncate_arrays_if_needed(source, max_array_length));
694            for (index, admission) in admissions.iter().enumerate() {
695                if *admission != Admission::Create {
696                    continue;
697                }
698                let version = version_of(index)
699                    .filter(|_| created.is_object())
700                    .map(|version| truncate_arrays_if_needed(version, max_array_length));
701                if let Some(view) = caches.get_mut(views[index].view_id) {
702                    view.store(
703                        key.to_string(),
704                        SharedEntity::from_parts(created.clone(), version),
705                    );
706                }
707                writes[index] = CacheWrite::Created;
708            }
709        }
710
711        let last_group = groups.len().saturating_sub(1);
712        for (group, (mut fields, members)) in groups.into_iter().enumerate() {
713            let mut group_patch = if group == last_group {
714                std::mem::take(&mut patch)
715            } else {
716                patch.clone()
717            };
718            let merge_once = fields.is_object() && group_patch.is_object();
719            if merge_once {
720                deep_merge_with_append(
721                    Arc::make_mut(&mut fields),
722                    std::mem::take(&mut group_patch),
723                    append_paths,
724                    max_array_length,
725                );
726            }
727            for index in members {
728                let Some(entity) = caches
729                    .get_mut(views[index].view_id)
730                    .and_then(|view| view.entities.peek_mut(key))
731                else {
732                    continue;
733                };
734                *entity.fields_mut() = fields.clone();
735                if merge_once {
736                    let version = entity.version_mut();
737                    *version = merge_version(
738                        version.take(),
739                        version_of(index),
740                        append_paths,
741                        max_array_length,
742                    );
743                } else {
744                    // Not two objects: the patch replaces the entity, or
745                    // merges into the array it is. Rare enough to do per view.
746                    merge_shared(
747                        entity,
748                        with_version(group_patch.clone(), version_of(index)),
749                        append_paths,
750                        max_array_length,
751                    );
752                }
753            }
754        }
755        writes
756    }
757
758    /// Store `entity` as the whole entity for `key`, replacing anything held
759    /// and clearing an eviction: the source vouched for it being complete
760    /// (see [`arete_interpreter::Mutation::mark_whole_entity`]).
761    pub async fn store_whole(&self, view_id: &str, key: &str, entity: Value) -> bool {
762        self.store_whole_for_lifetime(view_id, key, entity, None)
763            .await
764    }
765
766    /// Store a requested whole entity only if the request was made for the
767    /// account lifetime that is still current.
768    pub async fn store_whole_for_lifetime(
769        &self,
770        view_id: &str,
771        key: &str,
772        entity: Value,
773        requested_account_position: Option<AccountPosition>,
774    ) -> bool {
775        self.store_whole_checked(view_id, key, entity, requested_account_position, None)
776            .await
777    }
778
779    pub async fn store_whole_for_ordering(
780        &self,
781        view_id: &str,
782        key: &str,
783        entity: Value,
784        requested_account_position: Option<AccountPosition>,
785        requested_source_seq: Option<&str>,
786    ) -> bool {
787        self.store_whole_for_ordering_in_domain(
788            view_id,
789            key,
790            entity,
791            requested_account_position,
792            requested_source_seq,
793            SlotIndexDomain::Legacy,
794        )
795        .await
796    }
797
798    pub async fn store_whole_for_ordering_in_domain(
799        &self,
800        view_id: &str,
801        key: &str,
802        entity: Value,
803        requested_account_position: Option<AccountPosition>,
804        requested_source_seq: Option<&str>,
805        requested_source_domain: SlotIndexDomain,
806    ) -> bool {
807        self.store_whole_checked(
808            view_id,
809            key,
810            entity,
811            requested_account_position,
812            Some((requested_source_seq, requested_source_domain)),
813        )
814        .await
815    }
816
817    async fn store_whole_checked(
818        &self,
819        view_id: &str,
820        key: &str,
821        entity: Value,
822        requested_account_position: Option<AccountPosition>,
823        requested_source: Option<(Option<&str>, SlotIndexDomain)>,
824    ) -> bool {
825        let (entity, version) = split_version(entity);
826        self.store_whole_views(
827            key,
828            entity,
829            &[ViewVersion { view_id, version }],
830            requested_account_position,
831            requested_source,
832        )
833        .await
834        .pop()
835        .expect("store_whole_views answers for every view")
836    }
837
838    /// Store a whole entity in several views at once, each under its own
839    /// `_version`, where the checks of [`Self::store_whole_for_lifetime`]
840    /// (or, with `requested_source`,
841    /// [`Self::store_whole_for_ordering_in_domain`]) pass. `entity` holds every
842    /// other field; the views that store it share one copy.
843    pub(crate) async fn store_whole_views(
844        &self,
845        key: &str,
846        entity: Value,
847        views: &[ViewVersion<'_>],
848        requested_account_position: Option<AccountPosition>,
849        requested_source: Option<(Option<&str>, SlotIndexDomain)>,
850    ) -> Vec<bool> {
851        let max_array_length = self.config.max_array_length;
852        let (entity, stray_version) = split_version(entity);
853        let fields = Arc::new(truncate_arrays_if_needed(entity, max_array_length));
854        let mut caches = self.caches.write().await;
855        views
856            .iter()
857            .map(|view| {
858                let entries = caches
859                    .entry(view.view_id.to_string())
860                    .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
861                if !entries.accepts_whole(key, requested_account_position, requested_source) {
862                    return false;
863                }
864                let version = view
865                    .version
866                    .clone()
867                    .or_else(|| stray_version.clone())
868                    .filter(|_| fields.is_object())
869                    .map(|version| truncate_arrays_if_needed(version, max_array_length));
870                entries.store(
871                    key.to_string(),
872                    SharedEntity::from_parts(fields.clone(), version),
873                );
874                true
875            })
876            .collect()
877    }
878
879    pub async fn account_position(&self, view_id: &str, key: &str) -> Option<AccountPosition> {
880        self.caches
881            .read()
882            .await
883            .get(view_id)
884            .and_then(|view| view.lifetimes.peek(key))
885            .and_then(|checkpoint| checkpoint.account_position)
886    }
887
888    pub async fn accepts_mutation(
889        &self,
890        view_id: &str,
891        key: &str,
892        _entity: &Value,
893        creation: bool,
894    ) -> bool {
895        self.accepts_lifetime_mutation(view_id, key, creation, None)
896            .await
897    }
898
899    pub async fn accepts_lifetime_mutation(
900        &self,
901        view_id: &str,
902        key: &str,
903        creation: bool,
904        account_position: Option<AccountPosition>,
905    ) -> bool {
906        self.accepts_ordered_lifetime_mutation(view_id, key, creation, account_position, None)
907            .await
908    }
909
910    pub async fn accepts_ordered_lifetime_mutation(
911        &self,
912        view_id: &str,
913        key: &str,
914        creation: bool,
915        account_position: Option<AccountPosition>,
916        source_seq: Option<&str>,
917    ) -> bool {
918        self.accepts_ordered_lifetime_mutation_in_domain(
919            view_id,
920            key,
921            creation,
922            account_position,
923            source_seq,
924            SlotIndexDomain::Legacy,
925        )
926        .await
927    }
928
929    pub async fn accepts_ordered_lifetime_mutation_in_domain(
930        &self,
931        view_id: &str,
932        key: &str,
933        creation: bool,
934        account_position: Option<AccountPosition>,
935        source_seq: Option<&str>,
936        source_domain: SlotIndexDomain,
937    ) -> bool {
938        self.caches.read().await.get(view_id).is_none_or(|view| {
939            view.accepts(
940                key,
941                creation,
942                account_position,
943                source_seq,
944                Some(source_domain),
945            )
946        })
947    }
948
949    /// Apply an ordered source deletion. A delayed deletion never removes a
950    /// newer recreation. Only a newer, explicitly marked creation can restart
951    /// a deleted row; sparse patches and resends cannot resurrect it.
952    pub async fn delete(
953        &self,
954        view_id: &str,
955        key: &str,
956        account_position: Option<AccountPosition>,
957    ) -> bool {
958        match account_position {
959            Some(position) => {
960                self.delete_ordered(view_id, key, Some(position), None)
961                    .await
962            }
963            None => self.delete_current(view_id, key).await,
964        }
965    }
966
967    /// Delete the lifetime currently held by the cache when the producer has
968    /// no cursor to attach. This is an explicit, trusted operation: callers
969    /// with ordering evidence must use [`Self::delete_ordered`] instead.
970    pub async fn delete_current(&self, view_id: &str, key: &str) -> bool {
971        let mut caches = self.caches.write().await;
972        let view = caches
973            .entry(view_id.to_string())
974            .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
975        let checkpoint = view
976            .lifetimes
977            .peek(key)
978            .cloned()
979            .unwrap_or(EntityLifetimeCheckpoint {
980                account_position: None,
981                source_seq: None,
982                source_sequences: BTreeMap::new(),
983                deleted: false,
984            });
985        if checkpoint.deleted {
986            return true;
987        }
988        view.entities.pop(key);
989        view.forget_evicted(key);
990        view.lifetimes.put(
991            key.to_string(),
992            EntityLifetimeCheckpoint {
993                deleted: true,
994                ..checkpoint
995            },
996        );
997        view.trim_lifetimes();
998        true
999    }
1000
1001    /// Delete using the producer's own `_seq` ordering only when no
1002    /// authoritative account position is present.
1003    pub async fn delete_ordered(
1004        &self,
1005        view_id: &str,
1006        key: &str,
1007        account_position: Option<AccountPosition>,
1008        source_seq: Option<&str>,
1009    ) -> bool {
1010        self.delete_ordered_in_domain(
1011            view_id,
1012            key,
1013            account_position,
1014            source_seq,
1015            SlotIndexDomain::Legacy,
1016        )
1017        .await
1018    }
1019
1020    pub async fn delete_ordered_in_domain(
1021        &self,
1022        view_id: &str,
1023        key: &str,
1024        account_position: Option<AccountPosition>,
1025        source_seq: Option<&str>,
1026        source_domain: SlotIndexDomain,
1027    ) -> bool {
1028        let mut caches = self.caches.write().await;
1029        let view = caches
1030            .entry(view_id.to_string())
1031            .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1032        if let Some(checkpoint) = view.lifetimes.peek(key) {
1033            match (checkpoint.account_position, account_position) {
1034                (Some(previous), Some(incoming)) if incoming < previous => return false,
1035                (Some(previous), Some(incoming)) if incoming == previous => {
1036                    return checkpoint.deleted;
1037                }
1038                _ => {}
1039            }
1040        }
1041        if account_position.is_none()
1042            && source_seq.is_some()
1043            && !view.accepts_source_ordering(key, source_seq, Some(source_domain), true)
1044        {
1045            return false;
1046        }
1047        let previous = view.lifetimes.peek(key).cloned();
1048        view.entities.pop(key);
1049        view.forget_evicted(key);
1050        let mut checkpoint = if account_position.is_some() {
1051            let previous = previous.unwrap_or(EntityLifetimeCheckpoint {
1052                account_position: None,
1053                source_seq: None,
1054                source_sequences: BTreeMap::new(),
1055                deleted: false,
1056            });
1057            EntityLifetimeCheckpoint {
1058                account_position,
1059                source_seq: previous.source_seq,
1060                source_sequences: previous.source_sequences,
1061                deleted: true,
1062            }
1063        } else {
1064            previous.unwrap_or(EntityLifetimeCheckpoint {
1065                account_position: None,
1066                source_seq: None,
1067                source_sequences: BTreeMap::new(),
1068                deleted: false,
1069            })
1070        };
1071        if account_position.is_none() {
1072            if let Some(source_seq) = source_seq {
1073                checkpoint
1074                    .source_sequences
1075                    .insert(source_domain, source_seq.to_string());
1076            }
1077            checkpoint.deleted = true;
1078        }
1079        view.lifetimes.put(key.to_string(), checkpoint);
1080        view.trim_lifetimes();
1081        true
1082    }
1083
1084    pub async fn dump_lifetimes(&self) -> EntityLifetimes {
1085        self.caches
1086            .read()
1087            .await
1088            .iter()
1089            .flat_map(|(id, view)| {
1090                view.lifetimes
1091                    .iter()
1092                    .map(|(key, checkpoint)| (id.clone(), key.clone(), checkpoint.clone()))
1093            })
1094            .collect()
1095    }
1096
1097    pub async fn hydrate_lifetimes(&self, lifetimes: EntityLifetimes) {
1098        let mut caches = self.caches.write().await;
1099        for (id, key, checkpoint) in lifetimes.into_iter().rev() {
1100            let view = caches
1101                .entry(id)
1102                .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1103            view.lifetimes.put(key, checkpoint);
1104            view.trim_lifetimes();
1105        }
1106    }
1107
1108    /// Restore the old `_seq` barriers as account positions. Those barriers
1109    /// were written only by authoritative account deletion/recreation paths;
1110    /// whether the row exists distinguishes a live recreation from deletion.
1111    pub async fn hydrate_legacy_tombstones(&self, tombstones: EntityTombstones) {
1112        let mut caches = self.caches.write().await;
1113        for (id, key, seq) in tombstones.into_iter().rev() {
1114            let view = caches
1115                .entry(id)
1116                .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1117            let deleted = !view.entities.contains(&key);
1118            let account_position = seq.as_deref().and_then(account_position_from_seq);
1119            view.lifetimes.put(
1120                key,
1121                EntityLifetimeCheckpoint {
1122                    account_position,
1123                    source_seq: None,
1124                    source_sequences: BTreeMap::new(),
1125                    deleted,
1126                },
1127            );
1128            view.trim_lifetimes();
1129        }
1130    }
1131
1132    /// Whether the shared cache says a published delete still represents the
1133    /// latest lifetime. Subscription tasks use this without mutating the cache
1134    /// a second time after the projector has already applied the delete.
1135    pub async fn deletion_is_current(&self, view_id: &str, key: &str) -> bool {
1136        let caches = self.caches.read().await;
1137        caches.get(view_id).is_none_or(|view| {
1138            view.lifetimes
1139                .peek(key)
1140                .map_or(!view.entities.contains(key), |checkpoint| {
1141                    checkpoint.deleted
1142                })
1143        })
1144    }
1145
1146    /// Merge a source patch into `base` with this cache's append and array
1147    /// rules, for callers that keep their own full copy of an entity.
1148    pub fn merge_patch(&self, base: &mut Value, patch: Value, append_paths: &[String]) {
1149        deep_merge_with_append(base, patch, append_paths, self.config.max_array_length);
1150    }
1151
1152    /// [`Self::merge_patch`] for a [`SharedEntity`]: copies its fields first
1153    /// if another holder shares them, so the merge never reaches that holder.
1154    pub fn merge_patch_shared(
1155        &self,
1156        entity: &mut SharedEntity,
1157        patch: Value,
1158        append_paths: &[String],
1159    ) {
1160        merge_shared(entity, patch, append_paths, self.config.max_array_length);
1161    }
1162
1163    /// Treat `keys` as evicted from `view_id` unless the view holds them, so
1164    /// patches for them from a source that does not mark creations are
1165    /// refused. Used after a restore, when the VM holds entities the restored
1166    /// cache does not; a VM that marks creations makes this moot, and the
1167    /// view drops the memory with its first patch.
1168    pub async fn remember_evicted(&self, view_id: &str, keys: impl IntoIterator<Item = String>) {
1169        let mut caches = self.caches.write().await;
1170        let view = caches
1171            .entry(view_id.to_string())
1172            .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1173        for key in keys {
1174            if !view.entities.contains(&key) {
1175                view.remember_evicted(key);
1176            }
1177        }
1178    }
1179
1180    /// Get all cached entities for a view.
1181    ///
1182    /// Returns a vector of (key, entity) pairs for sending as snapshots
1183    /// to new subscribers. Each is a copy; [`Self::get_all_shared`] returns
1184    /// the cached entities themselves.
1185    pub async fn get_all(&self, view_id: &str) -> Vec<(String, Value)> {
1186        self.get_all_shared(view_id)
1187            .await
1188            .into_iter()
1189            .map(|(key, entity)| (key, entity.into_value()))
1190            .collect()
1191    }
1192
1193    /// Every entity cached for a view, most recently used first, sharing
1194    /// their fields with the cache rather than copying them.
1195    pub async fn get_all_shared(&self, view_id: &str) -> Vec<(String, SharedEntity)> {
1196        let caches = self.caches.read().await;
1197
1198        caches
1199            .get(view_id)
1200            .map(|view| {
1201                view.entities
1202                    .iter()
1203                    .map(|(k, v)| (k.clone(), v.clone()))
1204                    .collect()
1205            })
1206            .unwrap_or_default()
1207    }
1208
1209    /// Get entities with _seq greater than the provided cursor.
1210    ///
1211    /// Returns entities that have been updated after the given cursor,
1212    /// sorted by _seq in ascending order. Useful for resuming from
1213    /// a specific point in the stream.
1214    pub async fn get_after(
1215        &self,
1216        view_id: &str,
1217        cursor: &str,
1218        limit: Option<usize>,
1219    ) -> Vec<(String, Value)> {
1220        let caches = self.caches.read().await;
1221
1222        if let Some(view) = caches.get(view_id) {
1223            let mut results: Vec<(String, &SharedEntity)> = view
1224                .entities
1225                .iter()
1226                .filter(|(_, entity)| {
1227                    entity
1228                        .field("_seq")
1229                        .and_then(|s| s.as_str())
1230                        .map(|seq| cmp_seq(seq, cursor) == std::cmp::Ordering::Greater)
1231                        .unwrap_or(false)
1232                })
1233                .map(|(k, v)| (k.clone(), v))
1234                .collect();
1235
1236            // Sort by _seq (ascending)
1237            results.sort_by(|a, b| {
1238                let seq_a = a.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1239                let seq_b = b.1.field("_seq").and_then(|s| s.as_str()).unwrap_or("");
1240                cmp_seq(seq_a, seq_b)
1241            });
1242
1243            // Apply limit if provided
1244            if let Some(limit) = limit {
1245                results.truncate(limit);
1246            }
1247
1248            results
1249                .into_iter()
1250                .map(|(key, entity)| (key, entity.to_value()))
1251                .collect()
1252        } else {
1253            vec![]
1254        }
1255    }
1256
1257    /// Get a copy of a specific entity from the cache.
1258    pub async fn get(&self, view_id: &str, key: &str) -> Option<Value> {
1259        self.get_shared(view_id, key)
1260            .await
1261            .map(SharedEntity::into_value)
1262    }
1263
1264    /// A specific cached entity, sharing its fields with the cache rather
1265    /// than copying them.
1266    pub async fn get_shared(&self, view_id: &str, key: &str) -> Option<SharedEntity> {
1267        let caches = self.caches.read().await;
1268        caches
1269            .get(view_id)
1270            .and_then(|view| view.entities.peek(key).cloned())
1271    }
1272
1273    /// Remove one entity after a source-wide delete.
1274    ///
1275    /// The delete ends the entity, so the key is also forgotten as evicted: a
1276    /// later patch for it creates a new entity.
1277    pub async fn remove(&self, view_id: &str, key: &str) -> Option<Value> {
1278        let removed = {
1279            let mut caches = self.caches.write().await;
1280            caches.get_mut(view_id).and_then(|view| {
1281                view.forget_evicted(key);
1282                view.entities.pop(key)
1283            })
1284        };
1285        removed.map(SharedEntity::into_value)
1286    }
1287
1288    /// Get the number of cached entities for a view
1289    pub async fn len(&self, view_id: &str) -> usize {
1290        let caches = self.caches.read().await;
1291        caches
1292            .get(view_id)
1293            .map(|view| view.entities.len())
1294            .unwrap_or(0)
1295    }
1296
1297    /// Check if the cache for a view is empty
1298    pub async fn is_empty(&self, view_id: &str) -> bool {
1299        self.len(view_id).await == 0
1300    }
1301
1302    /// Get the snapshot batch configuration
1303    pub fn snapshot_config(&self) -> SnapshotBatchConfig {
1304        SnapshotBatchConfig {
1305            initial_batch_size: self.config.initial_snapshot_batch_size,
1306            subsequent_batch_size: self.config.subsequent_snapshot_batch_size,
1307        }
1308    }
1309
1310    /// Clear all cached entities for a view
1311    pub async fn clear(&self, view_id: &str) {
1312        let mut caches = self.caches.write().await;
1313        if let Some(view) = caches.get_mut(view_id) {
1314            view.entities.clear();
1315            view.evicted = None;
1316        }
1317    }
1318
1319    pub async fn clear_all(&self) {
1320        let mut caches = self.caches.write().await;
1321        caches.clear();
1322    }
1323
1324    /// Dump every view's entities for a state snapshot.
1325    ///
1326    /// Entries are ordered most-recently-used first; [`Self::hydrate`] relies
1327    /// on that to reconstruct LRU eviction order.
1328    pub async fn dump(&self) -> Vec<(String, Vec<(String, Value)>)> {
1329        let caches = self.caches.read().await;
1330        caches
1331            .iter()
1332            .map(|(view_id, view)| {
1333                (
1334                    view_id.clone(),
1335                    view.entities
1336                        .iter()
1337                        .map(|(key, entity)| (key.clone(), entity.to_value()))
1338                        .collect(),
1339                )
1340            })
1341            .collect()
1342    }
1343
1344    /// Restore entities dumped by [`Self::dump`], preserving LRU order.
1345    ///
1346    /// Entities are inserted as-is (no merge): a snapshot holds fully
1347    /// projected entities, not patches. A snapshot saves each view's copy of
1348    /// an entity on its own, so views of one export (`{export}/{mode}`) whose
1349    /// copies have the same fields (all but `_version`) share them again, as
1350    /// they did when saved. Only copies of one export are compared.
1351    pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
1352        let mut caches = self.caches.write().await;
1353        caches.clear();
1354        let mut restored: HashMap<(String, String), Vec<Arc<Value>>> = HashMap::new();
1355        for (view_id, entries) in views {
1356            let export = view_id.split('/').next().unwrap_or_default().to_string();
1357            let view = caches
1358                .entry(view_id)
1359                .or_insert_with(|| ViewEntries::new(self.config.max_entities_per_view));
1360            for (key, entity) in entries.into_iter().rev() {
1361                let (fields, version) = SharedEntity::new(entity).into_parts();
1362                let seen = restored.entry((export.clone(), key.clone())).or_default();
1363                let fields = match seen.iter().find(|held| **held == fields) {
1364                    Some(held) => held.clone(),
1365                    None => {
1366                        seen.push(fields.clone());
1367                        fields
1368                    }
1369                };
1370                view.store(key, SharedEntity::from_parts(fields, version));
1371            }
1372        }
1373    }
1374
1375    pub async fn stats(&self) -> CacheStats {
1376        let caches = self.caches.read().await;
1377        let mut total_entities = 0;
1378        let mut views = Vec::new();
1379
1380        for (view_id, view) in caches.iter() {
1381            let count = view.entities.len();
1382            total_entities += count;
1383            views.push((view_id.clone(), count));
1384        }
1385
1386        views.sort_by_key(|b| std::cmp::Reverse(b.1));
1387
1388        CacheStats {
1389            view_count: caches.len(),
1390            total_entities,
1391            top_views: views.into_iter().take(5).collect(),
1392        }
1393    }
1394}
1395
1396/// A point-in-time count of an [`EntityCache`]'s contents.
1397#[derive(Debug, Clone, PartialEq, Eq)]
1398pub struct CacheStats {
1399    pub view_count: usize,
1400    pub total_entities: usize,
1401    pub top_views: Vec<(String, usize)>,
1402}
1403
1404#[derive(Debug, Clone, Copy)]
1405pub struct SnapshotBatchConfig {
1406    pub initial_batch_size: usize,
1407    pub subsequent_batch_size: usize,
1408}
1409
1410impl Default for EntityCache {
1411    fn default() -> Self {
1412        Self::new()
1413    }
1414}
1415
1416fn deep_merge_with_append(
1417    base: &mut Value,
1418    patch: Value,
1419    append_paths: &[String],
1420    max_array_length: usize,
1421) {
1422    deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
1423}
1424
1425fn deep_merge_with_append_inner(
1426    base: &mut Value,
1427    patch: Value,
1428    append_paths: &[String],
1429    current_path: &str,
1430    max_array_length: usize,
1431) {
1432    match (base, patch) {
1433        (Value::Object(base_map), Value::Object(patch_map)) => {
1434            for (key, patch_value) in patch_map {
1435                let child_path = if current_path.is_empty() {
1436                    key.clone()
1437                } else {
1438                    format!("{}.{}", current_path, key)
1439                };
1440
1441                if let Some(base_value) = base_map.get_mut(&key) {
1442                    deep_merge_with_append_inner(
1443                        base_value,
1444                        patch_value,
1445                        append_paths,
1446                        &child_path,
1447                        max_array_length,
1448                    );
1449                } else {
1450                    base_map.insert(
1451                        key,
1452                        truncate_arrays_if_needed(patch_value, max_array_length),
1453                    );
1454                }
1455            }
1456        }
1457
1458        (Value::Array(base_arr), Value::Array(patch_arr)) => {
1459            let should_append = append_paths.iter().any(|p| p == current_path);
1460            if should_append {
1461                base_arr.extend(patch_arr);
1462                if base_arr.len() > max_array_length {
1463                    let excess = base_arr.len() - max_array_length;
1464                    base_arr.drain(0..excess);
1465                }
1466            } else {
1467                *base_arr = patch_arr;
1468                if base_arr.len() > max_array_length {
1469                    let excess = base_arr.len() - max_array_length;
1470                    base_arr.drain(0..excess);
1471                }
1472            }
1473        }
1474
1475        (base, patch_value) => {
1476            *base = truncate_arrays_if_needed(patch_value, max_array_length);
1477        }
1478    }
1479}
1480
1481/// Recursively truncate any arrays in a value to the max length, keeping
1482/// the newest (last) elements.
1483fn truncate_arrays_if_needed(mut value: Value, max_array_length: usize) -> Value {
1484    truncate_arrays_in_place(&mut value, max_array_length);
1485    value
1486}
1487
1488/// [`truncate_arrays_if_needed`] without rebuilding anything that needs no
1489/// truncation.
1490fn truncate_arrays_in_place(value: &mut Value, max_array_length: usize) {
1491    match value {
1492        Value::Array(arr) => {
1493            if arr.len() > max_array_length {
1494                let excess = arr.len() - max_array_length;
1495                arr.drain(0..excess);
1496            }
1497            for element in arr {
1498                truncate_arrays_in_place(element, max_array_length);
1499            }
1500        }
1501        Value::Object(map) => {
1502            for field in map.values_mut() {
1503                truncate_arrays_in_place(field, max_array_length);
1504            }
1505        }
1506        _ => {}
1507    }
1508}
1509
1510/// Merge `patch` into `entity` exactly as [`deep_merge_with_append`] would
1511/// merge it into the whole entity, `_version` included. The fields are copied
1512/// first if another holder shares them.
1513fn merge_shared(
1514    entity: &mut SharedEntity,
1515    patch: Value,
1516    append_paths: &[String],
1517    max_array_length: usize,
1518) {
1519    let (patch, patch_version) = split_version(patch);
1520    if entity.fields().is_object() && patch.is_object() {
1521        deep_merge_with_append(
1522            Arc::make_mut(entity.fields_mut()),
1523            patch,
1524            append_paths,
1525            max_array_length,
1526        );
1527        let version = entity.version_mut();
1528        *version = merge_version(
1529            version.take(),
1530            patch_version,
1531            append_paths,
1532            max_array_length,
1533        );
1534        return;
1535    }
1536    // Not two objects: the patch replaces the entity, unless both are arrays
1537    // and it merges into the one held. Only an object has a `_version`, so
1538    // the entity's own goes with it.
1539    let (fields, _) = std::mem::replace(entity, SharedEntity::new(Value::Null)).into_parts();
1540    let mut whole = match (&*fields, &patch) {
1541        (Value::Array(_), Value::Array(_)) => {
1542            Arc::try_unwrap(fields).unwrap_or_else(|shared| Value::clone(&shared))
1543        }
1544        _ => Value::Null,
1545    };
1546    deep_merge_with_append(
1547        &mut whole,
1548        with_version(patch, patch_version),
1549        append_paths,
1550        max_array_length,
1551    );
1552    *entity = SharedEntity::new(whole);
1553}
1554
1555/// The `_version` an entity has after a patch: the patch's, merged into the
1556/// entity's as [`deep_merge_with_append`] merges any top-level field.
1557fn merge_version(
1558    current: Option<Value>,
1559    incoming: Option<Value>,
1560    append_paths: &[String],
1561    max_array_length: usize,
1562) -> Option<Value> {
1563    match (current, incoming) {
1564        (Some(mut current), Some(incoming)) => {
1565            deep_merge_with_append_inner(
1566                &mut current,
1567                incoming,
1568                append_paths,
1569                VERSION_FIELD,
1570                max_array_length,
1571            );
1572            Some(current)
1573        }
1574        (None, Some(incoming)) => Some(truncate_arrays_if_needed(incoming, max_array_length)),
1575        (current, None) => current,
1576    }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581    use super::*;
1582    use serde_json::json;
1583
1584    #[tokio::test]
1585    async fn same_slot_instruction_activity_cannot_block_authoritative_deletion() {
1586        let cache = EntityCache::new();
1587        assert_eq!(
1588            cache
1589                .upsert_with_lifetime(
1590                    "v",
1591                    "a",
1592                    json!({"account":true,"_seq":"100:000000000009"}),
1593                    &[],
1594                    PatchOrigin::Creation,
1595                    Some(AccountPosition::new(100, 9)),
1596                )
1597                .await,
1598            CacheWrite::Created
1599        );
1600        assert_eq!(
1601            cache
1602                .upsert_with_append(
1603                    "v",
1604                    "a",
1605                    json!({"instruction":true,"_seq":"100:000000000900"}),
1606                    &[],
1607                    PatchOrigin::Change
1608                )
1609                .await,
1610            CacheWrite::Merged
1611        );
1612        assert!(
1613            cache
1614                .delete("v", "a", Some(AccountPosition::new(100, 10)))
1615                .await
1616        );
1617        assert!(cache.get("v", "a").await.is_none());
1618        assert!(cache.deletion_is_current("v", "a").await);
1619    }
1620
1621    #[tokio::test]
1622    async fn recreation_has_its_own_ordering_and_replaces_old_fields() {
1623        let cache = EntityCache::with_config(EntityCacheConfig {
1624            max_entities_per_view: 1,
1625            ..Default::default()
1626        });
1627        assert!(
1628            cache
1629                .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1630                .await
1631        );
1632        assert_eq!(
1633            cache
1634                .upsert_with_lifetime(
1635                    "v",
1636                    "a",
1637                    json!({"fresh":true, "_seq":"100:000000001002"}),
1638                    &[],
1639                    PatchOrigin::Creation,
1640                    Some(AccountPosition::new(100, 1002)),
1641                )
1642                .await,
1643            CacheWrite::Created
1644        );
1645        assert_eq!(
1646            cache
1647                .upsert_with_append(
1648                    "v",
1649                    "a",
1650                    json!({"instruction":true, "_seq":"100:000000000900"}),
1651                    &[],
1652                    PatchOrigin::Change
1653                )
1654                .await,
1655            CacheWrite::Merged
1656        );
1657
1658        // Neither a stale authoritative write/tombstone nor a delayed resend
1659        // from the deleted lifetime can contaminate the replacement.
1660        assert!(matches!(
1661            cache
1662                .upsert_with_lifetime(
1663                    "v",
1664                    "a",
1665                    json!({"old":true, "_seq":"100:000000001001"}),
1666                    &[],
1667                    PatchOrigin::Change,
1668                    Some(AccountPosition::new(100, 1001)),
1669                )
1670                .await,
1671            CacheWrite::Refused { .. }
1672        ));
1673        assert!(
1674            !cache
1675                .delete("v", "a", Some(AccountPosition::new(100, 1001)))
1676                .await
1677        );
1678        assert!(
1679            !cache
1680                .store_whole("v", "a", json!({"old":true, "_seq":"100:000000000900"}))
1681                .await
1682        );
1683        let row = cache.get("v", "a").await.unwrap();
1684        assert_eq!(row["fresh"], true);
1685        assert_eq!(row["instruction"], true);
1686        assert!(row.get("old").is_none());
1687
1688        cache
1689            .upsert_with_append(
1690                "v",
1691                "other",
1692                json!({"other":true}),
1693                &[],
1694                PatchOrigin::Creation,
1695            )
1696            .await;
1697        assert!(cache.get("v", "a").await.is_none());
1698        assert!(matches!(
1699            cache
1700                .upsert_with_lifetime(
1701                    "v",
1702                    "a",
1703                    json!({"accountUpdate":true, "_seq":"100:000000001003"}),
1704                    &[],
1705                    PatchOrigin::Change,
1706                    Some(AccountPosition::new(100, 1003)),
1707                )
1708                .await,
1709            CacheWrite::Refused { .. }
1710        ));
1711        assert!(
1712            !cache
1713                .store_whole_for_lifetime(
1714                    "v",
1715                    "a",
1716                    row.clone(),
1717                    Some(AccountPosition::new(100, 1002)),
1718                )
1719                .await
1720        );
1721        let mut current = row;
1722        current["accountUpdate"] = Value::Bool(true);
1723        current["_seq"] = Value::String("100:000000001003".into());
1724        assert!(
1725            cache
1726                .store_whole_for_lifetime("v", "a", current, Some(AccountPosition::new(100, 1003)),)
1727                .await
1728        );
1729
1730        // Current lifetime metadata survives a snapshot and continues to
1731        // admit instruction-domain offsets while rejecting stale account data.
1732        let restored = EntityCache::new();
1733        restored.hydrate(cache.dump().await).await;
1734        restored
1735            .hydrate_lifetimes(cache.dump_lifetimes().await)
1736            .await;
1737        assert_eq!(
1738            restored
1739                .upsert_with_append(
1740                    "v",
1741                    "a",
1742                    json!({"afterRestore":true, "_seq":"100:000000000901"}),
1743                    &[],
1744                    PatchOrigin::Change,
1745                )
1746                .await,
1747            CacheWrite::Merged
1748        );
1749        assert!(matches!(
1750            restored
1751                .upsert_with_lifetime(
1752                    "v",
1753                    "a",
1754                    json!({"old":true, "_seq":"100:000000001001"}),
1755                    &[],
1756                    PatchOrigin::Change,
1757                    Some(AccountPosition::new(100, 1001)),
1758                )
1759                .await,
1760            CacheWrite::Refused { .. }
1761        ));
1762    }
1763
1764    #[tokio::test]
1765    async fn recreation_retains_each_source_cursor_as_an_old_lifetime_barrier() {
1766        let cache = EntityCache::new();
1767        assert_eq!(
1768            cache
1769                .upsert_with_lifetime(
1770                    "v",
1771                    "a",
1772                    json!({"lifetime":"old"}),
1773                    &[],
1774                    PatchOrigin::Creation,
1775                    Some(AccountPosition::new(100, 9)),
1776                )
1777                .await,
1778            CacheWrite::Created
1779        );
1780        for (domain, seq, field) in [
1781            (
1782                SlotIndexDomain::Instruction,
1783                "100:000000000800",
1784                "oldInstruction",
1785            ),
1786            (SlotIndexDomain::Resolver, "100:900000000000", "oldResolver"),
1787        ] {
1788            let mut patch = json!({"_seq":seq});
1789            patch[field] = Value::Bool(true);
1790            assert_eq!(
1791                cache
1792                    .upsert_with_ordering(
1793                        "v",
1794                        "a",
1795                        patch,
1796                        &[],
1797                        PatchOrigin::Change,
1798                        LifetimeOrdering {
1799                            account_position: None,
1800                            source_seq: Some(seq),
1801                            source_domain: Some(domain),
1802                        },
1803                    )
1804                    .await,
1805                CacheWrite::Merged
1806            );
1807        }
1808
1809        assert!(
1810            cache
1811                .delete("v", "a", Some(AccountPosition::new(100, 10)))
1812                .await
1813        );
1814        assert_eq!(
1815            cache
1816                .upsert_with_lifetime(
1817                    "v",
1818                    "a",
1819                    json!({"lifetime":"new"}),
1820                    &[],
1821                    PatchOrigin::Creation,
1822                    Some(AccountPosition::new(100, 11)),
1823                )
1824                .await,
1825            CacheWrite::Created
1826        );
1827
1828        // Duplicates delayed across the deletion/recreation boundary retain
1829        // their old source positions and cannot change the replacement.
1830        for (domain, seq, field) in [
1831            (
1832                SlotIndexDomain::Instruction,
1833                "100:000000000799",
1834                "staleInstruction",
1835            ),
1836            (
1837                SlotIndexDomain::Resolver,
1838                "100:899999999999",
1839                "staleResolver",
1840            ),
1841        ] {
1842            let mut patch = json!({"_seq":seq});
1843            patch[field] = Value::Bool(true);
1844            assert!(matches!(
1845                cache
1846                    .upsert_with_ordering(
1847                        "v",
1848                        "a",
1849                        patch,
1850                        &[],
1851                        PatchOrigin::Change,
1852                        LifetimeOrdering {
1853                            account_position: None,
1854                            source_seq: Some(seq),
1855                            source_domain: Some(domain),
1856                        },
1857                    )
1858                    .await,
1859                CacheWrite::Refused { .. }
1860            ));
1861        }
1862
1863        // A later instruction in the same source domain remains valid even
1864        // though its txn index is lower than account write versions.
1865        assert_eq!(
1866            cache
1867                .upsert_with_ordering(
1868                    "v",
1869                    "a",
1870                    json!({"newInstruction":true, "_seq":"100:000000000900"}),
1871                    &[],
1872                    PatchOrigin::Change,
1873                    LifetimeOrdering {
1874                        account_position: None,
1875                        source_seq: Some("100:000000000900"),
1876                        source_domain: Some(SlotIndexDomain::Instruction),
1877                    },
1878                )
1879                .await,
1880            CacheWrite::Merged
1881        );
1882        let row = cache.get("v", "a").await.unwrap();
1883        assert_eq!(row["lifetime"], "new");
1884        assert_eq!(row["newInstruction"], true);
1885        assert!(row.get("oldInstruction").is_none());
1886        assert!(row.get("oldResolver").is_none());
1887        assert!(row.get("staleInstruction").is_none());
1888        assert!(row.get("staleResolver").is_none());
1889
1890        let restored = EntityCache::new();
1891        restored.hydrate(cache.dump().await).await;
1892        restored
1893            .hydrate_lifetimes(cache.dump_lifetimes().await)
1894            .await;
1895        assert!(matches!(
1896            restored
1897                .upsert_with_ordering(
1898                    "v",
1899                    "a",
1900                    json!({"staleAfterRestore":true}),
1901                    &[],
1902                    PatchOrigin::Change,
1903                    LifetimeOrdering {
1904                        account_position: None,
1905                        source_seq: Some("100:000000000799"),
1906                        source_domain: Some(SlotIndexDomain::Instruction),
1907                    },
1908                )
1909                .await,
1910            CacheWrite::Refused { .. }
1911        ));
1912    }
1913
1914    #[tokio::test]
1915    async fn markerless_sources_keep_seq_ordering_across_recreation_and_restore() {
1916        let cache = EntityCache::new();
1917        assert_eq!(
1918            cache
1919                .upsert_with_append(
1920                    "v",
1921                    "a",
1922                    json!({"old":true,"_seq":"100:000000000010"}),
1923                    &[],
1924                    PatchOrigin::Creation,
1925                )
1926                .await,
1927            CacheWrite::Created
1928        );
1929        assert!(
1930            !cache
1931                .delete_ordered("v", "a", None, Some("100:000000000009"))
1932                .await
1933        );
1934        assert!(cache.get("v", "a").await.is_some());
1935        assert!(
1936            cache
1937                .delete_ordered("v", "a", None, Some("100:000000000011"))
1938                .await
1939        );
1940        assert_eq!(
1941            cache
1942                .upsert_with_append(
1943                    "v",
1944                    "a",
1945                    json!({"fresh":true,"_seq":"100:000000000012"}),
1946                    &[],
1947                    PatchOrigin::Creation,
1948                )
1949                .await,
1950            CacheWrite::Created
1951        );
1952        assert!(matches!(
1953            cache
1954                .upsert_with_append(
1955                    "v",
1956                    "a",
1957                    json!({"stale":true,"_seq":"100:000000000010"}),
1958                    &[],
1959                    PatchOrigin::Creation,
1960                )
1961                .await,
1962            CacheWrite::Refused { .. }
1963        ));
1964        assert!(
1965            !cache
1966                .delete_ordered("v", "a", None, Some("100:000000000011"))
1967                .await
1968        );
1969
1970        let restored = EntityCache::new();
1971        restored.hydrate(cache.dump().await).await;
1972        restored
1973            .hydrate_lifetimes(cache.dump_lifetimes().await)
1974            .await;
1975        assert!(
1976            !restored
1977                .delete_ordered("v", "a", None, Some("100:000000000011"))
1978                .await
1979        );
1980        let row = restored.get("v", "a").await.unwrap();
1981        assert_eq!(row["fresh"], true);
1982        assert!(row.get("old").is_none());
1983        assert!(row.get("stale").is_none());
1984
1985        // An evicted row may be filled by a requested whole-entity resend,
1986        // but a response requested for the previous lifetime must not do it.
1987        let resync_cache = EntityCache::new();
1988        resync_cache
1989            .hydrate_lifetimes(cache.dump_lifetimes().await)
1990            .await;
1991        assert!(
1992            !resync_cache
1993                .store_whole_for_ordering(
1994                    "v",
1995                    "a",
1996                    json!({"stale_resend":true}),
1997                    None,
1998                    Some("100:000000000010"),
1999                )
2000                .await
2001        );
2002        assert!(
2003            resync_cache
2004                .store_whole_for_ordering(
2005                    "v",
2006                    "a",
2007                    json!({"fresh_resend":true}),
2008                    None,
2009                    Some("100:000000000012"),
2010                )
2011                .await
2012        );
2013    }
2014
2015    #[tokio::test]
2016    async fn older_source_patch_cannot_replace_newer_row_or_move_its_cursor_back() {
2017        let cache = EntityCache::new();
2018        assert_eq!(
2019            cache
2020                .upsert_with_append(
2021                    "v",
2022                    "a",
2023                    json!({"value":"created","_seq":"100:000000000010"}),
2024                    &[],
2025                    PatchOrigin::Creation,
2026                )
2027                .await,
2028            CacheWrite::Created
2029        );
2030        assert_eq!(
2031            cache
2032                .upsert_with_ordering(
2033                    "v",
2034                    "a",
2035                    json!({"value":"newer","_seq":"100:000000000012"}),
2036                    &[],
2037                    PatchOrigin::Change,
2038                    LifetimeOrdering {
2039                        account_position: None,
2040                        source_seq: Some("100:000000000012"),
2041                        source_domain: Some(SlotIndexDomain::Legacy),
2042                    },
2043                )
2044                .await,
2045            CacheWrite::Merged
2046        );
2047        assert!(matches!(
2048            cache
2049                .upsert_with_ordering(
2050                    "v",
2051                    "a",
2052                    json!({"value":"older","stale":true,"_seq":"100:000000000011"}),
2053                    &[],
2054                    PatchOrigin::Change,
2055                    LifetimeOrdering {
2056                        account_position: None,
2057                        source_seq: Some("100:000000000011"),
2058                        source_domain: Some(SlotIndexDomain::Legacy),
2059                    },
2060                )
2061                .await,
2062            CacheWrite::Refused { .. }
2063        ));
2064        assert!(
2065            !cache
2066                .delete_ordered("v", "a", None, Some("100:000000000011"))
2067                .await
2068        );
2069        let row = cache.get("v", "a").await.unwrap();
2070        assert_eq!(row["value"], "newer");
2071        assert_eq!(row["_seq"], "100:000000000012");
2072        assert!(row.get("stale").is_none());
2073    }
2074
2075    #[tokio::test]
2076    async fn independent_source_recency_domains_survive_snapshot_restore() {
2077        let cache = EntityCache::new();
2078        cache
2079            .upsert_with_lifetime(
2080                "v",
2081                "a",
2082                json!({"value":"created"}),
2083                &[],
2084                PatchOrigin::Creation,
2085                Some(AccountPosition::new(100, 9)),
2086            )
2087            .await;
2088        for (domain, seq, value) in [
2089            (SlotIndexDomain::Resolver, "100:900000000000", "resolver"),
2090            (
2091                SlotIndexDomain::Instruction,
2092                "100:000000000900",
2093                "instruction",
2094            ),
2095        ] {
2096            assert_eq!(
2097                cache
2098                    .upsert_with_ordering(
2099                        "v",
2100                        "a",
2101                        json!({"value":value, "_seq":seq}),
2102                        &[],
2103                        PatchOrigin::Change,
2104                        LifetimeOrdering {
2105                            account_position: None,
2106                            source_seq: Some(seq),
2107                            source_domain: Some(domain),
2108                        },
2109                    )
2110                    .await,
2111                CacheWrite::Merged
2112            );
2113        }
2114
2115        let restored = EntityCache::new();
2116        restored.hydrate(cache.dump().await).await;
2117        restored
2118            .hydrate_lifetimes(cache.dump_lifetimes().await)
2119            .await;
2120
2121        for (domain, seq) in [
2122            (SlotIndexDomain::Instruction, "100:000000000899"),
2123            (SlotIndexDomain::Resolver, "100:899999999999"),
2124        ] {
2125            assert!(matches!(
2126                restored
2127                    .upsert_with_ordering(
2128                        "v",
2129                        "a",
2130                        json!({"stale":true, "_seq":seq}),
2131                        &[],
2132                        PatchOrigin::Change,
2133                        LifetimeOrdering {
2134                            account_position: None,
2135                            source_seq: Some(seq),
2136                            source_domain: Some(domain),
2137                        },
2138                    )
2139                    .await,
2140                CacheWrite::Refused { .. }
2141            ));
2142        }
2143        assert_eq!(
2144            restored.get("v", "a").await.unwrap()["value"],
2145            "instruction"
2146        );
2147    }
2148
2149    #[tokio::test]
2150    async fn explicit_current_delete_accepts_a_source_without_position() {
2151        let cache = EntityCache::new();
2152        cache
2153            .upsert_with_append(
2154                "v",
2155                "a",
2156                json!({"value":true,"_seq":"100:000000000010"}),
2157                &[],
2158                PatchOrigin::Creation,
2159            )
2160            .await;
2161
2162        assert!(cache.delete("v", "a", None).await);
2163        assert!(cache.get("v", "a").await.is_none());
2164        assert!(cache.deletion_is_current("v", "a").await);
2165    }
2166
2167    #[tokio::test]
2168    async fn evicted_live_row_does_not_make_an_earlier_delete_current() {
2169        let cache = EntityCache::with_config(EntityCacheConfig {
2170            max_entities_per_view: 1,
2171            ..Default::default()
2172        });
2173        cache
2174            .upsert_with_lifetime(
2175                "v",
2176                "a",
2177                json!({"value":true,"_seq":"100:000000000010"}),
2178                &[],
2179                PatchOrigin::Creation,
2180                Some(AccountPosition::new(100, 10)),
2181            )
2182            .await;
2183        cache
2184            .upsert_with_append("v", "b", json!({"value":true}), &[], PatchOrigin::Creation)
2185            .await;
2186
2187        assert!(cache.get("v", "a").await.is_none());
2188        assert!(!cache.deletion_is_current("v", "a").await);
2189        assert!(cache.delete_current("v", "a").await);
2190        assert!(cache.deletion_is_current("v", "a").await);
2191    }
2192
2193    #[tokio::test]
2194    async fn test_basic_upsert_and_get() {
2195        let cache = EntityCache::new();
2196
2197        cache
2198            .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
2199            .await;
2200
2201        let entity = cache.get("tokens/list", "abc123").await;
2202        assert!(entity.is_some());
2203        assert_eq!(entity.unwrap()["name"], "Test Token");
2204    }
2205
2206    #[tokio::test]
2207    async fn test_deep_merge_objects() {
2208        let cache = EntityCache::new();
2209
2210        cache
2211            .upsert(
2212                "tokens/list",
2213                "abc123",
2214                json!({
2215                    "id": "abc123",
2216                    "metrics": {"volume": 100}
2217                }),
2218            )
2219            .await;
2220
2221        cache
2222            .upsert(
2223                "tokens/list",
2224                "abc123",
2225                json!({
2226                    "metrics": {"trades": 50}
2227                }),
2228            )
2229            .await;
2230
2231        let entity = cache.get("tokens/list", "abc123").await.unwrap();
2232        assert_eq!(entity["id"], "abc123");
2233        assert_eq!(entity["metrics"]["volume"], 100);
2234        assert_eq!(entity["metrics"]["trades"], 50);
2235    }
2236
2237    #[tokio::test]
2238    async fn test_array_append() {
2239        let cache = EntityCache::new();
2240
2241        cache
2242            .upsert(
2243                "tokens/list",
2244                "abc123",
2245                json!({
2246                    "events": [{"type": "buy", "amount": 100}]
2247                }),
2248            )
2249            .await;
2250
2251        cache
2252            .upsert_with_append(
2253                "tokens/list",
2254                "abc123",
2255                json!({
2256                    "events": [{"type": "sell", "amount": 50}]
2257                }),
2258                &["events".to_string()],
2259                PatchOrigin::Unknown,
2260            )
2261            .await;
2262
2263        let entity = cache.get("tokens/list", "abc123").await.unwrap();
2264        let events = entity["events"].as_array().unwrap();
2265        assert_eq!(events.len(), 2);
2266        assert_eq!(events[0]["type"], "buy");
2267        assert_eq!(events[1]["type"], "sell");
2268    }
2269
2270    #[tokio::test]
2271    async fn test_array_lru_eviction() {
2272        let config = EntityCacheConfig {
2273            max_entities_per_view: 1000,
2274            max_array_length: 3,
2275            ..Default::default()
2276        };
2277        let cache = EntityCache::with_config(config);
2278
2279        cache
2280            .upsert(
2281                "tokens/list",
2282                "abc123",
2283                json!({
2284                    "events": [
2285                        {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
2286                    ]
2287                }),
2288            )
2289            .await;
2290
2291        let entity = cache.get("tokens/list", "abc123").await.unwrap();
2292        let events = entity["events"].as_array().unwrap();
2293
2294        assert_eq!(events.len(), 3);
2295        assert_eq!(events[0]["id"], 3);
2296        assert_eq!(events[1]["id"], 4);
2297        assert_eq!(events[2]["id"], 5);
2298    }
2299
2300    #[tokio::test]
2301    async fn test_array_append_with_lru() {
2302        let config = EntityCacheConfig {
2303            max_entities_per_view: 1000,
2304            max_array_length: 3,
2305            ..Default::default()
2306        };
2307        let cache = EntityCache::with_config(config);
2308
2309        cache
2310            .upsert(
2311                "tokens/list",
2312                "abc123",
2313                json!({
2314                    "events": [{"id": 1}, {"id": 2}]
2315                }),
2316            )
2317            .await;
2318
2319        cache
2320            .upsert_with_append(
2321                "tokens/list",
2322                "abc123",
2323                json!({
2324                    "events": [{"id": 3}, {"id": 4}]
2325                }),
2326                &["events".to_string()],
2327                PatchOrigin::Unknown,
2328            )
2329            .await;
2330
2331        let entity = cache.get("tokens/list", "abc123").await.unwrap();
2332        let events = entity["events"].as_array().unwrap();
2333
2334        // [1,2] + [3,4] = [1,2,3,4] → LRU(3) = [2,3,4]
2335        assert_eq!(events.len(), 3);
2336        assert_eq!(events[0]["id"], 2);
2337        assert_eq!(events[1]["id"], 3);
2338        assert_eq!(events[2]["id"], 4);
2339    }
2340
2341    #[tokio::test]
2342    async fn test_entity_lru_eviction() {
2343        let config = EntityCacheConfig {
2344            max_entities_per_view: 2,
2345            max_array_length: 100,
2346            ..Default::default()
2347        };
2348        let cache = EntityCache::with_config(config);
2349
2350        cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2351        cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2352        cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
2353
2354        assert!(cache.get("tokens/list", "key1").await.is_none());
2355        assert!(cache.get("tokens/list", "key2").await.is_some());
2356        assert!(cache.get("tokens/list", "key3").await.is_some());
2357    }
2358
2359    #[tokio::test]
2360    async fn a_patch_for_an_evicted_key_is_refused_not_stored() {
2361        let cache = EntityCache::with_config(EntityCacheConfig {
2362            max_entities_per_view: 2,
2363            ..Default::default()
2364        });
2365        for id in 1..=3 {
2366            assert_eq!(
2367                cache
2368                    .upsert(
2369                        "tokens/list",
2370                        &format!("key{id}"),
2371                        json!({"id": id, "n": 0})
2372                    )
2373                    .await,
2374                CacheWrite::Created
2375            );
2376        }
2377        // key1 was evicted; this carries only what changed.
2378        assert_eq!(
2379            cache.upsert("tokens/list", "key1", json!({"n": 5})).await,
2380            CacheWrite::Refused {
2381                patch: json!({"n": 5})
2382            }
2383        );
2384        assert!(cache.get("tokens/list", "key1").await.is_none());
2385        assert_eq!(
2386            cache.upsert("tokens/list", "key3", json!({"n": 1})).await,
2387            CacheWrite::Merged
2388        );
2389
2390        // A source delete ends the entity: it may then be created again.
2391        cache.remove("tokens/list", "key1").await;
2392        assert_eq!(
2393            cache
2394                .upsert("tokens/list", "key1", json!({"id": 1, "n": 6}))
2395                .await,
2396            CacheWrite::Created
2397        );
2398    }
2399
2400    #[tokio::test]
2401    async fn a_whole_entity_ends_an_eviction() {
2402        let cache = EntityCache::with_config(EntityCacheConfig {
2403            max_entities_per_view: 1,
2404            ..Default::default()
2405        });
2406        cache.upsert("v", "a", json!({"id": "a", "n": 0})).await;
2407        cache.upsert("v", "b", json!({"id": "b", "n": 0})).await;
2408        assert!(matches!(
2409            cache.upsert("v", "a", json!({"n": 1})).await,
2410            CacheWrite::Refused { .. }
2411        ));
2412        cache
2413            .store_whole("v", "a", json!({"id": "a", "n": 1}))
2414            .await;
2415        assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2416        assert_eq!(cache.len("v").await, 1, "still bounded: b made room");
2417        assert_eq!(
2418            cache.upsert("v", "a", json!({"n": 2})).await,
2419            CacheWrite::Merged
2420        );
2421    }
2422
2423    #[tokio::test]
2424    async fn the_evicted_key_memory_is_bounded() {
2425        let cache = EntityCache::with_config(EntityCacheConfig {
2426            max_entities_per_view: 1,
2427            ..Default::default()
2428        });
2429        let remembered = EVICTED_KEYS_PER_CACHED_ENTITY;
2430        for key in 0..=remembered + 1 {
2431            cache
2432                .upsert("v", &key.to_string(), json!({"id": key}))
2433                .await;
2434        }
2435        // Key 0 was pushed out of the memory by the evictions after it; the
2436        // most recent evictions are still remembered.
2437        assert_eq!(
2438            cache.upsert("v", "0", json!({"id": 0})).await,
2439            CacheWrite::Created
2440        );
2441        assert!(matches!(
2442            cache
2443                .upsert("v", &remembered.to_string(), json!({"n": 1}))
2444                .await,
2445            CacheWrite::Refused { .. }
2446        ));
2447    }
2448
2449    async fn write(
2450        cache: &EntityCache,
2451        key: &str,
2452        patch: Value,
2453        origin: PatchOrigin,
2454    ) -> CacheWrite {
2455        cache.upsert_with_append("v", key, patch, &[], origin).await
2456    }
2457
2458    /// A source that marks creations says outright which patches are whole:
2459    /// no memory of evictions is needed, and none can run out.
2460    #[tokio::test]
2461    async fn an_unmarked_patch_for_a_key_not_held_is_refused_however_old() {
2462        let cache = EntityCache::with_config(EntityCacheConfig {
2463            max_entities_per_view: 1,
2464            ..Default::default()
2465        });
2466        let evictions = EVICTED_KEYS_PER_CACHED_ENTITY * 4;
2467        for key in 0..=evictions {
2468            assert_eq!(
2469                write(
2470                    &cache,
2471                    &key.to_string(),
2472                    json!({"id": key}),
2473                    PatchOrigin::Creation
2474                )
2475                .await,
2476                CacheWrite::Created,
2477                "a creation is stored as soon as it arrives"
2478            );
2479        }
2480        // Key 0 was evicted far longer ago than any memory would reach.
2481        assert_eq!(
2482            write(&cache, "0", json!({"n": 1}), PatchOrigin::Change).await,
2483            CacheWrite::Refused {
2484                patch: json!({"n": 1})
2485            }
2486        );
2487        assert!(
2488            matches!(
2489                write(&cache, "never", json!({"n": 1}), PatchOrigin::Change).await,
2490                CacheWrite::Refused { .. }
2491            ),
2492            "nor does it matter whether the view ever held the key"
2493        );
2494        assert_eq!(cache.get("v", "0").await, None);
2495        assert_eq!(
2496            write(
2497                &cache,
2498                &evictions.to_string(),
2499                json!({"n": 1}),
2500                PatchOrigin::Change
2501            )
2502            .await,
2503            CacheWrite::Merged
2504        );
2505        // The source started key 0 again.
2506        assert_eq!(
2507            write(&cache, "0", json!({"id": 0, "n": 2}), PatchOrigin::Creation).await,
2508            CacheWrite::Created
2509        );
2510        assert_eq!(cache.get("v", "0").await, Some(json!({"id": 0, "n": 2})));
2511    }
2512
2513    /// A creation for a key the view still holds starts a new lifetime and
2514    /// replaces fields that no longer exist.
2515    #[tokio::test]
2516    async fn a_creation_for_a_key_held_replaces() {
2517        let cache = EntityCache::new();
2518        write(
2519            &cache,
2520            "a",
2521            json!({"id": "a", "old": 1}),
2522            PatchOrigin::Creation,
2523        )
2524        .await;
2525        assert_eq!(
2526            write(
2527                &cache,
2528                "a",
2529                json!({"id": "a", "n": 1}),
2530                PatchOrigin::Creation
2531            )
2532            .await,
2533            CacheWrite::Created
2534        );
2535        assert_eq!(cache.get("v", "a").await, Some(json!({"id": "a", "n": 1})));
2536    }
2537
2538    async fn remembers_evictions(cache: &EntityCache, view_id: &str) -> bool {
2539        cache
2540            .caches
2541            .read()
2542            .await
2543            .get(view_id)
2544            .is_some_and(|view| view.evicted.is_some())
2545    }
2546
2547    /// A view fed by a source that marks creations never consults the
2548    /// evicted-key memory, so it drops what a restore put there and keeps no
2549    /// more.
2550    #[tokio::test]
2551    async fn a_view_fed_marked_creations_keeps_no_eviction_memory() {
2552        let cache = EntityCache::with_config(EntityCacheConfig {
2553            max_entities_per_view: 1,
2554            ..Default::default()
2555        });
2556        cache.remember_evicted("v", ["restored".to_string()]).await;
2557        assert!(remembers_evictions(&cache, "v").await);
2558
2559        write(&cache, "a", json!({"id": "a"}), PatchOrigin::Creation).await;
2560        assert!(!remembers_evictions(&cache, "v").await);
2561        write(&cache, "b", json!({"id": "b"}), PatchOrigin::Creation).await;
2562        assert!(!remembers_evictions(&cache, "v").await, "a was evicted");
2563        assert!(matches!(
2564            write(&cache, "restored", json!({"n": 1}), PatchOrigin::Change).await,
2565            CacheWrite::Refused { .. }
2566        ));
2567        assert!(matches!(
2568            write(&cache, "a", json!({"n": 1}), PatchOrigin::Change).await,
2569            CacheWrite::Refused { .. }
2570        ));
2571    }
2572
2573    #[tokio::test]
2574    async fn remembered_keys_are_refused_unless_held() {
2575        let cache = EntityCache::new();
2576        cache.upsert("tokens/list", "held", json!({"id": 1})).await;
2577        cache
2578            .remember_evicted("tokens/list", ["held".to_string(), "gone".to_string()])
2579            .await;
2580        assert_eq!(
2581            cache.upsert("tokens/list", "held", json!({"n": 1})).await,
2582            CacheWrite::Merged
2583        );
2584        assert!(matches!(
2585            cache.upsert("tokens/list", "gone", json!({"n": 1})).await,
2586            CacheWrite::Refused { .. }
2587        ));
2588        // Other views are unaffected.
2589        assert_eq!(
2590            cache.upsert("tokens/state", "gone", json!({"id": 2})).await,
2591            CacheWrite::Created
2592        );
2593    }
2594
2595    #[tokio::test]
2596    async fn test_get_all() {
2597        let cache = EntityCache::new();
2598
2599        cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2600        cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
2601
2602        let all = cache.get_all("tokens/list").await;
2603        assert_eq!(all.len(), 2);
2604    }
2605
2606    #[tokio::test]
2607    async fn remove_is_scoped_to_one_entity() {
2608        let cache = EntityCache::new();
2609        cache.upsert("tokens/list", "one", json!({"id": 1})).await;
2610        cache.upsert("tokens/list", "two", json!({"id": 2})).await;
2611
2612        assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
2613        assert!(cache.get("tokens/list", "one").await.is_none());
2614        assert!(cache.get("tokens/list", "two").await.is_some());
2615    }
2616
2617    #[tokio::test]
2618    async fn test_separate_views() {
2619        let cache = EntityCache::new();
2620
2621        cache
2622            .upsert("tokens/list", "key1", json!({"type": "token"}))
2623            .await;
2624        cache
2625            .upsert("games/list", "key1", json!({"type": "game"}))
2626            .await;
2627
2628        let token = cache.get("tokens/list", "key1").await.unwrap();
2629        let game = cache.get("games/list", "key1").await.unwrap();
2630
2631        assert_eq!(token["type"], "token");
2632        assert_eq!(game["type"], "game");
2633    }
2634
2635    #[test]
2636    fn test_deep_merge_with_append() {
2637        let mut base = json!({
2638            "a": 1,
2639            "b": {"c": 2},
2640            "arr": [1, 2]
2641        });
2642
2643        let patch = json!({
2644            "b": {"d": 3},
2645            "arr": [3],
2646            "e": 4
2647        });
2648
2649        deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
2650
2651        assert_eq!(base["a"], 1);
2652        assert_eq!(base["b"]["c"], 2);
2653        assert_eq!(base["b"]["d"], 3);
2654        assert_eq!(base["arr"].as_array().unwrap().len(), 3);
2655        assert_eq!(base["e"], 4);
2656    }
2657
2658    #[test]
2659    fn test_deep_merge_replace_array() {
2660        let mut base = json!({
2661            "arr": [1, 2, 3]
2662        });
2663
2664        let patch = json!({
2665            "arr": [4, 5]
2666        });
2667
2668        deep_merge_with_append(&mut base, patch, &[], 100);
2669
2670        assert_eq!(base["arr"].as_array().unwrap().len(), 2);
2671        assert_eq!(base["arr"][0], 4);
2672        assert_eq!(base["arr"][1], 5);
2673    }
2674
2675    #[test]
2676    fn test_deep_merge_nested_append() {
2677        let mut base = json!({
2678            "stats": {"events": [1, 2]}
2679        });
2680
2681        let patch = json!({
2682            "stats": {"events": [3]}
2683        });
2684
2685        deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
2686
2687        assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
2688    }
2689
2690    #[test]
2691    fn test_snapshot_config_defaults() {
2692        let cache = EntityCache::new();
2693        let config = cache.snapshot_config();
2694
2695        assert_eq!(config.initial_batch_size, 50);
2696        assert_eq!(config.subsequent_batch_size, 100);
2697    }
2698
2699    #[test]
2700    fn test_snapshot_config_custom() {
2701        let config = EntityCacheConfig {
2702            initial_snapshot_batch_size: 25,
2703            subsequent_snapshot_batch_size: 75,
2704            ..Default::default()
2705        };
2706        let cache = EntityCache::with_config(config);
2707        let snapshot_config = cache.snapshot_config();
2708
2709        assert_eq!(snapshot_config.initial_batch_size, 25);
2710        assert_eq!(snapshot_config.subsequent_batch_size, 75);
2711    }
2712
2713    #[tokio::test]
2714    async fn test_get_after() {
2715        let cache = EntityCache::new();
2716
2717        // Insert entities with _seq values
2718        cache
2719            .upsert(
2720                "tokens/list",
2721                "key1",
2722                json!({"id": 1, "_seq": "100:000000000001"}),
2723            )
2724            .await;
2725        cache
2726            .upsert(
2727                "tokens/list",
2728                "key2",
2729                json!({"id": 2, "_seq": "100:000000000002"}),
2730            )
2731            .await;
2732        cache
2733            .upsert(
2734                "tokens/list",
2735                "key3",
2736                json!({"id": 3, "_seq": "100:000000000003"}),
2737            )
2738            .await;
2739        cache
2740            .upsert(
2741                "tokens/list",
2742                "key4",
2743                json!({"id": 4, "_seq": "101:000000000001"}),
2744            )
2745            .await;
2746
2747        // Get all entities after "100:000000000002"
2748        let after = cache
2749            .get_after("tokens/list", "100:000000000002", None)
2750            .await;
2751
2752        // Should return key3 and key4 (sorted by _seq)
2753        assert_eq!(after.len(), 2);
2754        assert_eq!(after[0].0, "key3");
2755        assert_eq!(after[1].0, "key4");
2756    }
2757
2758    #[tokio::test]
2759    async fn test_get_after_with_limit() {
2760        let cache = EntityCache::new();
2761
2762        // Insert entities with _seq values
2763        cache
2764            .upsert(
2765                "tokens/list",
2766                "key1",
2767                json!({"id": 1, "_seq": "100:000000000001"}),
2768            )
2769            .await;
2770        cache
2771            .upsert(
2772                "tokens/list",
2773                "key2",
2774                json!({"id": 2, "_seq": "100:000000000002"}),
2775            )
2776            .await;
2777        cache
2778            .upsert(
2779                "tokens/list",
2780                "key3",
2781                json!({"id": 3, "_seq": "100:000000000003"}),
2782            )
2783            .await;
2784
2785        // Get entities after "100:000000000000" with limit 2
2786        let after = cache
2787            .get_after("tokens/list", "100:000000000000", Some(2))
2788            .await;
2789
2790        // Should return only first 2 (key1 and key2)
2791        assert_eq!(after.len(), 2);
2792        assert_eq!(after[0].0, "key1");
2793        assert_eq!(after[1].0, "key2");
2794    }
2795
2796    #[tokio::test]
2797    async fn test_get_after_empty_result() {
2798        let cache = EntityCache::new();
2799
2800        cache
2801            .upsert(
2802                "tokens/list",
2803                "key1",
2804                json!({"id": 1, "_seq": "100:000000000001"}),
2805            )
2806            .await;
2807
2808        // Get entities after a future cursor
2809        let after = cache
2810            .get_after("tokens/list", "999:000000000000", None)
2811            .await;
2812
2813        assert!(after.is_empty());
2814    }
2815
2816    #[tokio::test]
2817    async fn test_get_after_missing_seq() {
2818        let cache = EntityCache::new();
2819
2820        // Insert entity without _seq
2821        cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
2822
2823        // Get entities after any cursor - entity without _seq should not be included
2824        let after = cache.get_after("tokens/list", "0:000000000000", None).await;
2825
2826        assert!(after.is_empty());
2827    }
2828
2829    fn versions<'a>(views: &[(&'a str, &str)]) -> Vec<ViewVersion<'a>> {
2830        views
2831            .iter()
2832            .map(|(view_id, version)| ViewVersion {
2833                view_id,
2834                version: Some(json!(version)),
2835            })
2836            .collect()
2837    }
2838
2839    const VIEWS: [&str; 3] = ["t/list", "t/state", "t/append"];
2840
2841    async fn shared(cache: &EntityCache, key: &str) -> Vec<SharedEntity> {
2842        let mut entities = Vec::new();
2843        for view in VIEWS {
2844            entities.push(cache.get_shared(view, key).await.unwrap());
2845        }
2846        entities
2847    }
2848
2849    async fn write_all(
2850        cache: &EntityCache,
2851        key: &str,
2852        patch: Value,
2853        stamps: [&str; 3],
2854        origin: PatchOrigin,
2855    ) -> Vec<CacheWrite> {
2856        let views: Vec<_> = VIEWS.into_iter().zip(stamps).collect();
2857        cache
2858            .upsert_views(
2859                key,
2860                patch,
2861                &versions(&views),
2862                &["events".to_string()],
2863                origin,
2864                LifetimeOrdering::default(),
2865            )
2866            .await
2867    }
2868
2869    /// Views written together hold one copy of the entity's fields, each
2870    /// under its own `_version`, and every view still reads as the whole
2871    /// entity its frames describe.
2872    #[tokio::test]
2873    async fn views_written_together_share_one_copy() {
2874        let cache = EntityCache::new();
2875        let writes = write_all(
2876            &cache,
2877            "a",
2878            json!({"id": "a", "events": [1]}),
2879            ["e:1", "e:2", "e:3"],
2880            PatchOrigin::Creation,
2881        )
2882        .await;
2883        assert_eq!(writes, vec![CacheWrite::Created; 3]);
2884        let [list, state, append] =
2885            <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
2886        assert!(list.shares_fields_with(&state) && list.shares_fields_with(&append));
2887        // Three views and the three copies just read.
2888        assert_eq!(Arc::strong_count(list.fields()), 6);
2889        assert_eq!(
2890            cache.get("t/state", "a").await,
2891            Some(json!({"id": "a", "events": [1], "_version": "e:2"}))
2892        );
2893
2894        let writes = write_all(
2895            &cache,
2896            "a",
2897            json!({"n": 1, "events": [2]}),
2898            ["e:4", "e:5", "e:6"],
2899            PatchOrigin::Unknown,
2900        )
2901        .await;
2902        assert_eq!(writes, vec![CacheWrite::Merged; 3]);
2903        let merged = shared(&cache, "a").await;
2904        assert!(
2905            merged[0].shares_fields_with(&merged[1]) && merged[0].shares_fields_with(&merged[2])
2906        );
2907        for (view, version) in VIEWS.into_iter().zip(["e:4", "e:5", "e:6"]) {
2908            assert_eq!(
2909                cache.get(view, "a").await,
2910                Some(json!({"id": "a", "n": 1, "events": [1, 2], "_version": version})),
2911                "{view}"
2912            );
2913        }
2914        // The copies read before the merge still hold what they held.
2915        assert_eq!(
2916            list.to_value(),
2917            json!({"id": "a", "events": [1], "_version": "e:1"})
2918        );
2919        assert!(!list.shares_fields_with(&merged[0]));
2920    }
2921
2922    /// Nothing else holds the views' shared fields, so the merge writes them
2923    /// in place instead of copying them.
2924    #[tokio::test]
2925    async fn a_merge_nobody_else_holds_happens_in_place() {
2926        let cache = EntityCache::new();
2927        write_all(
2928            &cache,
2929            "a",
2930            json!({"id": "a"}),
2931            ["e:1", "e:2", "e:3"],
2932            PatchOrigin::Creation,
2933        )
2934        .await;
2935        let before = Arc::as_ptr(cache.get_shared("t/list", "a").await.unwrap().fields());
2936        write_all(
2937            &cache,
2938            "a",
2939            json!({"n": 1}),
2940            ["e:4", "e:5", "e:6"],
2941            PatchOrigin::Unknown,
2942        )
2943        .await;
2944        let after = cache.get_shared("t/list", "a").await.unwrap();
2945        // Had the merge copied the fields, the copy would have been made
2946        // while the original was still allocated, at another address.
2947        assert_eq!(Arc::as_ptr(after.fields()), before);
2948        assert_eq!(Arc::strong_count(after.fields()), 4);
2949        assert_eq!(after["n"], 1);
2950    }
2951
2952    /// A patch written to one view leaves another view's copy as it was,
2953    /// even though the two shared their fields until then.
2954    #[tokio::test]
2955    async fn a_patch_to_one_view_does_not_reach_another() {
2956        let cache = EntityCache::new();
2957        write_all(
2958            &cache,
2959            "a",
2960            json!({"id": "a", "nested": {"n": 0}}),
2961            ["e:1", "e:2", "e:3"],
2962            PatchOrigin::Creation,
2963        )
2964        .await;
2965        assert_eq!(
2966            cache
2967                .upsert_with_append(
2968                    "t/list",
2969                    "a",
2970                    json!({"nested": {"n": 1}, "_version": "e:4"}),
2971                    &[],
2972                    PatchOrigin::Unknown,
2973                )
2974                .await,
2975            CacheWrite::Merged
2976        );
2977        assert_eq!(
2978            cache.get("t/list", "a").await,
2979            Some(json!({"id": "a", "nested": {"n": 1}, "_version": "e:4"}))
2980        );
2981        for (view, version) in [("t/state", "e:2"), ("t/append", "e:3")] {
2982            assert_eq!(
2983                cache.get(view, "a").await,
2984                Some(json!({"id": "a", "nested": {"n": 0}, "_version": version})),
2985                "{view}"
2986            );
2987        }
2988        let [list, state, append] =
2989            <[SharedEntity; 3]>::try_from(shared(&cache, "a").await).unwrap();
2990        assert!(!list.shares_fields_with(&state));
2991        assert!(state.shares_fields_with(&append));
2992    }
2993
2994    /// A view that refuses a patch keeps its copy while the views that take
2995    /// it move on to a new one.
2996    #[tokio::test]
2997    async fn a_view_that_refuses_a_patch_keeps_its_copy() {
2998        let cache = EntityCache::new();
2999        write_all(
3000            &cache,
3001            "a",
3002            json!({"id": "a", "n": 0}),
3003            ["e:1", "e:2", "e:3"],
3004            PatchOrigin::Creation,
3005        )
3006        .await;
3007        // Only the state view saw the entity's deletion.
3008        assert!(cache.delete_current("t/state", "a").await);
3009        let writes = write_all(
3010            &cache,
3011            "a",
3012            json!({"n": 1}),
3013            ["e:4", "e:5", "e:6"],
3014            PatchOrigin::Change,
3015        )
3016        .await;
3017        assert_eq!(writes[0], CacheWrite::Merged);
3018        assert_eq!(
3019            writes[1],
3020            CacheWrite::Refused {
3021                patch: json!({"n": 1, "_version": "e:5"})
3022            }
3023        );
3024        assert_eq!(writes[2], CacheWrite::Merged);
3025        assert_eq!(cache.get("t/state", "a").await, None);
3026        let list = cache.get_shared("t/list", "a").await.unwrap();
3027        let append = cache.get_shared("t/append", "a").await.unwrap();
3028        assert!(list.shares_fields_with(&append));
3029        assert_eq!(
3030            append.to_value(),
3031            json!({"id": "a", "n": 1, "_version": "e:6"})
3032        );
3033    }
3034
3035    /// A snapshot saves each view's copy of an entity on its own; restoring
3036    /// it shares the fields again and keeps every view's `_version`.
3037    #[tokio::test]
3038    async fn a_restore_shares_the_copies_it_saved_per_view() {
3039        let cache = EntityCache::new();
3040        write_all(
3041            &cache,
3042            "a",
3043            json!({"id": "a", "events": [1, 2]}),
3044            ["e:1", "e:2", "e:3"],
3045            PatchOrigin::Creation,
3046        )
3047        .await;
3048        let mut dump = cache.dump().await;
3049        // Another export's entity under the same key is not compared, even
3050        // when it happens to have the same fields.
3051        dump.push((
3052            "other/list".to_string(),
3053            vec![("a".to_string(), json!({"id": "a", "events": [1, 2]}))],
3054        ));
3055        dump.sort_by(|left, right| left.0.cmp(&right.0));
3056
3057        let restored = EntityCache::new();
3058        restored.hydrate(dump.clone()).await;
3059        let other = restored.get_shared("other/list", "a").await.unwrap();
3060        let copies = shared(&restored, "a").await;
3061        assert!(!other.shares_fields_with(&copies[0]));
3062        assert!(
3063            copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3064        );
3065        let mut again = restored.dump().await;
3066        again.sort_by(|left, right| left.0.cmp(&right.0));
3067        assert_eq!(again, dump);
3068    }
3069
3070    /// A whole entity stored in several views at once is one copy.
3071    #[tokio::test]
3072    async fn a_whole_entity_stored_in_several_views_is_one_copy() {
3073        let cache = EntityCache::new();
3074        let views = versions(&[("t/list", "e:1"), ("t/state", "e:2"), ("t/append", "e:3")]);
3075        let stored = cache
3076            .store_whole_views("a", json!({"id": "a"}), &views, None, None)
3077            .await;
3078        assert_eq!(stored, vec![true; 3]);
3079        let copies = shared(&cache, "a").await;
3080        assert!(
3081            copies[0].shares_fields_with(&copies[1]) && copies[0].shares_fields_with(&copies[2])
3082        );
3083        assert_eq!(
3084            cache.get("t/append", "a").await,
3085            Some(json!({"id": "a", "_version": "e:3"}))
3086        );
3087    }
3088
3089    /// Merging into a shared copy that is not an object (or with a patch that
3090    /// is not one) still follows the whole-value merge rules.
3091    #[test]
3092    fn a_shared_merge_matches_the_whole_value_merge() {
3093        let cases = [
3094            (
3095                json!({"a": 1, "_version": "e:1"}),
3096                json!({"b": 2, "_version": "e:2"}),
3097            ),
3098            (json!({"a": 1, "_version": "e:1"}), json!({"b": 2})),
3099            (json!({"a": 1, "_version": "e:1"}), json!([1, 2])),
3100            (json!([1, 2]), json!([3])),
3101            (json!([1, 2]), json!({"a": 1, "_version": "e:2"})),
3102            (json!("text"), json!(5)),
3103            (
3104                json!({"_version": "e:1", "list": [1]}),
3105                json!({"list": [2, 3, 4]}),
3106            ),
3107        ];
3108        for (base, patch) in cases {
3109            let append = ["list".to_string()];
3110            let mut expected = base.clone();
3111            deep_merge_with_append(&mut expected, patch.clone(), &append, 2);
3112            let mut entity = SharedEntity::new(base.clone());
3113            let other = entity.clone();
3114            merge_shared(&mut entity, patch.clone(), &append, 2);
3115            assert_eq!(entity.to_value(), expected, "{base} <- {patch}");
3116            assert_eq!(other.to_value(), base, "the other holder's copy");
3117        }
3118    }
3119}