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