Skip to main content

graphrefly/adapters/
reactive_collection_storage.rs

1//! Graph-bound persistence sidecars for reactive collections (D161).
2//!
3//! Storage remains passive: load helpers live in `storage`, restore helpers live
4//! with the data structures, and this adapter only composes collection deltas
5//! into strict JSON storage frames plus observable persistence facts.
6
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10use serde::de::DeserializeOwned;
11use serde::Serialize;
12use serde_json::{json, Value};
13
14use crate::data_structures::{
15    restore_reactive_index, restore_reactive_list, restore_reactive_log, restore_reactive_map,
16    IndexChange, IndexRow, ListChange, LogChange, MapChange, ReactiveIndex, ReactiveIndexOptions,
17    ReactiveList, ReactiveListOptions, ReactiveLog, ReactiveLogOptions, ReactiveMap,
18    ReactiveMapOptions,
19};
20use crate::graph::{Graph, GraphNodeOpts};
21use crate::node::Node;
22use crate::protocol::Message;
23use crate::storage::{
24    load_reactive_index_state, load_reactive_list_state, load_reactive_log_state,
25    load_reactive_map_state, reactive_collection_change_frame, reactive_collection_snapshot_frame,
26    reactive_collection_snapshot_key, AppendLogStorageTier, KvStorageTier,
27    LoadReactiveCollectionStateOptions, ReactiveCollectionChangeFrame, ReactiveCollectionKind,
28    ReactiveCollectionSnapshotFrame, StorageError, StorageResult,
29};
30
31type Disposer = Box<dyn FnOnce()>;
32
33#[derive(Clone, Debug, Eq, PartialEq)]
34/// `ReactiveCollectionPersistenceStatus` variants.
35pub enum ReactiveCollectionPersistenceStatus {
36    /// `Starting` variant.
37    Starting,
38    /// `Ready` variant.
39    Ready,
40    /// `Flushing` variant.
41    Flushing,
42    /// `Errored` variant.
43    Errored,
44    /// `Disposed` variant.
45    Disposed,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49/// `ReactiveCollectionPersistenceCursor` data container.
50pub struct ReactiveCollectionPersistenceCursor {
51    /// `collection` field for collection.
52    pub collection: ReactiveCollectionKind,
53    /// `change_seq` field for change seq.
54    pub change_seq: Option<u64>,
55    /// `snapshot_writes` field for snapshot writes.
56    pub snapshot_writes: usize,
57    /// `change_writes` field for change writes.
58    pub change_writes: usize,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62/// `ReactiveCollectionPersistenceStatusFact` data container.
63pub struct ReactiveCollectionPersistenceStatusFact {
64    /// `state` field for state.
65    pub state: ReactiveCollectionPersistenceStatus,
66    /// `pending` field for pending.
67    pub pending: usize,
68    /// `writes` field for writes.
69    pub writes: usize,
70    /// `errors` field for errors.
71    pub errors: usize,
72    /// `cursor` field for cursor.
73    pub cursor: ReactiveCollectionPersistenceCursor,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77/// `ReactiveCollectionPersistenceErrorFact` data container.
78pub struct ReactiveCollectionPersistenceErrorFact {
79    /// `phase` field for phase.
80    pub phase: String,
81    /// `message` field for message.
82    pub message: String,
83    /// `cursor` field for cursor.
84    pub cursor: ReactiveCollectionPersistenceCursor,
85}
86
87#[derive(Clone)]
88/// `PersistReactiveCollectionOptions` data container.
89pub struct PersistReactiveCollectionOptions {
90    /// `graph` field for graph.
91    pub graph: Option<Graph>,
92    /// `name` field for name.
93    pub name: Option<String>,
94    /// `storage_prefix` field for storage prefix.
95    pub storage_prefix: Option<String>,
96    /// `snapshot_key` field for snapshot key.
97    pub snapshot_key: Option<String>,
98    /// `snapshot_store` field for snapshot store.
99    pub snapshot_store: Rc<dyn KvStorageTier<ReactiveCollectionSnapshotFrame>>,
100    /// `change_log` field for change log.
101    pub change_log: Option<Rc<dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>>,
102    /// `snapshot_on_attach` field for snapshot on attach.
103    pub snapshot_on_attach: bool,
104    /// `snapshot_every_changes` field for snapshot every changes.
105    pub snapshot_every_changes: Option<usize>,
106}
107
108impl PersistReactiveCollectionOptions {
109    /// Creates or computes `new`.
110    pub fn new(
111        snapshot_store: Rc<dyn KvStorageTier<ReactiveCollectionSnapshotFrame>>,
112        storage_prefix: impl Into<String>,
113    ) -> Self {
114        Self {
115            graph: None,
116            name: None,
117            storage_prefix: Some(storage_prefix.into()),
118            snapshot_key: None,
119            snapshot_store,
120            change_log: None,
121            snapshot_on_attach: true,
122            snapshot_every_changes: None,
123        }
124    }
125}
126
127/// `ReactiveCollectionPersistence` data container.
128pub struct ReactiveCollectionPersistence {
129    /// `ready` field for ready.
130    pub ready: Node<bool>,
131    /// `status` field for status.
132    pub status: Node<Value>,
133    /// `error` field for error.
134    pub error: Node<Value>,
135    /// `cursor` field for cursor.
136    pub cursor: Node<Value>,
137    flush: Rc<dyn Fn() -> StorageResult<()>>,
138    snapshot: Rc<dyn Fn() -> StorageResult<()>>,
139    dispose: RefCell<Option<Disposer>>,
140}
141
142impl ReactiveCollectionPersistence {
143    /// Updates or reads `flush`.
144    pub fn flush(&self) -> StorageResult<()> {
145        (self.flush)()
146    }
147
148    /// Updates or reads `snapshot`.
149    pub fn snapshot(&self) -> StorageResult<()> {
150        (self.snapshot)()
151    }
152
153    /// Updates or reads `dispose`.
154    pub fn dispose(&self) {
155        if let Some(dispose) = self.dispose.borrow_mut().take() {
156            dispose();
157        }
158    }
159
160    /// Updates or reads `status_fact`.
161    pub fn status_fact(&self) -> StorageResult<ReactiveCollectionPersistenceStatusFact> {
162        let value = self
163            .status
164            .cache()
165            .ok_or_else(|| StorageError::backend("reactiveCollection status fact is absent"))?;
166        parse_status_fact(&value)
167    }
168
169    /// Updates or reads `cursor_fact`.
170    pub fn cursor_fact(&self) -> StorageResult<ReactiveCollectionPersistenceCursor> {
171        let value = self
172            .cursor
173            .cache()
174            .ok_or_else(|| StorageError::backend("reactiveCollection cursor fact is absent"))?;
175        parse_cursor_fact(&value)
176    }
177
178    /// Updates or reads `error_fact`.
179    pub fn error_fact(&self) -> StorageResult<Option<ReactiveCollectionPersistenceErrorFact>> {
180        let Some(value) = self.error.cache() else {
181            return Ok(None);
182        };
183        if value.is_null() {
184            return Ok(None);
185        }
186        parse_error_fact(&value).map(Some)
187    }
188}
189
190impl Drop for ReactiveCollectionPersistence {
191    fn drop(&mut self) {
192        if let Some(dispose) = self.dispose.borrow_mut().take() {
193            dispose();
194        }
195    }
196}
197
198/// `OpenPersistentReactiveList` data container.
199pub struct OpenPersistentReactiveList<T> {
200    /// `collection` field for collection.
201    pub collection: ReactiveList<T>,
202    /// `persistence` field for persistence.
203    pub persistence: ReactiveCollectionPersistence,
204}
205
206/// `OpenPersistentReactiveLog` data container.
207pub struct OpenPersistentReactiveLog<T> {
208    /// `collection` field for collection.
209    pub collection: ReactiveLog<T>,
210    /// `persistence` field for persistence.
211    pub persistence: ReactiveCollectionPersistence,
212}
213
214/// `OpenPersistentReactiveMap` data container.
215pub struct OpenPersistentReactiveMap<K, V> {
216    /// `collection` field for collection.
217    pub collection: ReactiveMap<K, V>,
218    /// `persistence` field for persistence.
219    pub persistence: ReactiveCollectionPersistence,
220}
221
222/// `OpenPersistentReactiveIndex` data container.
223pub struct OpenPersistentReactiveIndex<K, S, V> {
224    /// `collection` field for collection.
225    pub collection: ReactiveIndex<K, S, V>,
226    /// `persistence` field for persistence.
227    pub persistence: ReactiveCollectionPersistence,
228}
229
230/// `OpenPersistentReactiveListOptions` data container.
231pub struct OpenPersistentReactiveListOptions<T> {
232    /// `initial` field for initial.
233    pub initial: Vec<T>,
234    /// `collection` field for collection.
235    pub collection: ReactiveListOptions,
236    /// `persistence` field for persistence.
237    pub persistence: PersistReactiveCollectionOptions,
238}
239
240/// `OpenPersistentReactiveLogOptions` data container.
241pub struct OpenPersistentReactiveLogOptions<T> {
242    /// `initial` field for initial.
243    pub initial: Vec<T>,
244    /// `collection` field for collection.
245    pub collection: ReactiveLogOptions,
246    /// `persistence` field for persistence.
247    pub persistence: PersistReactiveCollectionOptions,
248}
249
250/// `OpenPersistentReactiveMapOptions` data container.
251pub struct OpenPersistentReactiveMapOptions<K, V> {
252    /// `initial` field for initial.
253    pub initial: Vec<(K, V)>,
254    /// `collection` field for collection.
255    pub collection: ReactiveMapOptions,
256    /// `persistence` field for persistence.
257    pub persistence: PersistReactiveCollectionOptions,
258}
259
260/// `OpenPersistentReactiveIndexOptions` data container.
261pub struct OpenPersistentReactiveIndexOptions<K, S, V> {
262    /// `initial` field for initial.
263    pub initial: Vec<IndexRow<K, S, V>>,
264    /// `collection` field for collection.
265    pub collection: ReactiveIndexOptions,
266    /// `persistence` field for persistence.
267    pub persistence: PersistReactiveCollectionOptions,
268}
269
270/// Creates or computes `persist_reactive_list`.
271pub fn persist_reactive_list<T>(
272    collection: &ReactiveList<T>,
273    options: PersistReactiveCollectionOptions,
274) -> StorageResult<ReactiveCollectionPersistence>
275where
276    T: Clone + Serialize + 'static,
277{
278    persist_collection(
279        ReactiveCollectionKind::ReactiveList,
280        collection.to_vec(),
281        &collection.delta,
282        options,
283        None,
284        |change| serde_json::to_value(change).map_err(storage_json_error),
285        apply_list_snapshot_change::<T>,
286    )
287}
288
289/// Creates or computes `persist_reactive_log`.
290pub fn persist_reactive_log<T>(
291    collection: &ReactiveLog<T>,
292    options: PersistReactiveCollectionOptions,
293) -> StorageResult<ReactiveCollectionPersistence>
294where
295    T: Clone + Serialize + 'static,
296{
297    persist_collection(
298        ReactiveCollectionKind::ReactiveLog,
299        collection.to_vec(),
300        &collection.delta,
301        options,
302        None,
303        |change| serde_json::to_value(change).map_err(storage_json_error),
304        apply_log_snapshot_change::<T>,
305    )
306}
307
308/// Creates or computes `persist_reactive_map`.
309pub fn persist_reactive_map<K, V>(
310    collection: &ReactiveMap<K, V>,
311    options: PersistReactiveCollectionOptions,
312) -> StorageResult<ReactiveCollectionPersistence>
313where
314    K: Clone + Ord + std::fmt::Debug + Serialize + 'static,
315    V: Clone + Serialize + 'static,
316{
317    persist_collection(
318        ReactiveCollectionKind::ReactiveMap,
319        collection.to_map().into_iter().collect::<Vec<_>>(),
320        &collection.delta,
321        options,
322        None,
323        |change| serde_json::to_value(change).map_err(storage_json_error),
324        apply_map_snapshot_change::<K, V>,
325    )
326}
327
328/// Creates or computes `persist_reactive_index`.
329pub fn persist_reactive_index<K, S, V>(
330    collection: &ReactiveIndex<K, S, V>,
331    options: PersistReactiveCollectionOptions,
332) -> StorageResult<ReactiveCollectionPersistence>
333where
334    K: Clone + Ord + std::fmt::Debug + Serialize + 'static,
335    S: Clone + Ord + Serialize + 'static,
336    V: Clone + Serialize + 'static,
337{
338    persist_collection(
339        ReactiveCollectionKind::ReactiveIndex,
340        collection.to_vec(),
341        &collection.delta,
342        options,
343        None,
344        |change| serde_json::to_value(change).map_err(storage_json_error),
345        apply_index_snapshot_change::<K, S, V>,
346    )
347}
348
349fn persist_reactive_list_with_cursor<T>(
350    collection: &ReactiveList<T>,
351    options: PersistReactiveCollectionOptions,
352    initial_cursor: Option<u64>,
353) -> StorageResult<ReactiveCollectionPersistence>
354where
355    T: Clone + Serialize + 'static,
356{
357    persist_collection(
358        ReactiveCollectionKind::ReactiveList,
359        collection.to_vec(),
360        &collection.delta,
361        options,
362        initial_cursor,
363        |change| serde_json::to_value(change).map_err(storage_json_error),
364        apply_list_snapshot_change::<T>,
365    )
366}
367
368fn persist_reactive_log_with_cursor<T>(
369    collection: &ReactiveLog<T>,
370    options: PersistReactiveCollectionOptions,
371    initial_cursor: Option<u64>,
372) -> StorageResult<ReactiveCollectionPersistence>
373where
374    T: Clone + Serialize + 'static,
375{
376    persist_collection(
377        ReactiveCollectionKind::ReactiveLog,
378        collection.to_vec(),
379        &collection.delta,
380        options,
381        initial_cursor,
382        |change| serde_json::to_value(change).map_err(storage_json_error),
383        apply_log_snapshot_change::<T>,
384    )
385}
386
387fn persist_reactive_map_with_cursor<K, V>(
388    collection: &ReactiveMap<K, V>,
389    options: PersistReactiveCollectionOptions,
390    initial_cursor: Option<u64>,
391) -> StorageResult<ReactiveCollectionPersistence>
392where
393    K: Clone + Ord + std::fmt::Debug + Serialize + 'static,
394    V: Clone + Serialize + 'static,
395{
396    persist_collection(
397        ReactiveCollectionKind::ReactiveMap,
398        collection.to_map().into_iter().collect::<Vec<_>>(),
399        &collection.delta,
400        options,
401        initial_cursor,
402        |change| serde_json::to_value(change).map_err(storage_json_error),
403        apply_map_snapshot_change::<K, V>,
404    )
405}
406
407fn persist_reactive_index_with_cursor<K, S, V>(
408    collection: &ReactiveIndex<K, S, V>,
409    options: PersistReactiveCollectionOptions,
410    initial_cursor: Option<u64>,
411) -> StorageResult<ReactiveCollectionPersistence>
412where
413    K: Clone + Ord + std::fmt::Debug + Serialize + 'static,
414    S: Clone + Ord + Serialize + 'static,
415    V: Clone + Serialize + 'static,
416{
417    persist_collection(
418        ReactiveCollectionKind::ReactiveIndex,
419        collection.to_vec(),
420        &collection.delta,
421        options,
422        initial_cursor,
423        |change| serde_json::to_value(change).map_err(storage_json_error),
424        apply_index_snapshot_change::<K, S, V>,
425    )
426}
427
428/// Creates or computes `open_persistent_reactive_list`.
429pub fn open_persistent_reactive_list<T>(
430    options: OpenPersistentReactiveListOptions<T>,
431) -> StorageResult<OpenPersistentReactiveList<T>>
432where
433    T: Clone + Serialize + DeserializeOwned + 'static,
434{
435    let mut state = load_reactive_list_state(
436        options.persistence.snapshot_store.as_ref(),
437        LoadReactiveCollectionStateOptions {
438            storage_prefix: options.persistence.storage_prefix.as_deref(),
439            snapshot_key: options.persistence.snapshot_key.as_deref(),
440            change_log: options.persistence.change_log.as_deref(),
441        },
442    )?;
443    if !state.snapshot_found && state.changes_applied == 0 {
444        state.state = options.initial;
445    }
446    let initial_cursor = state.cursor;
447    let collection = restore_reactive_list(state, options.collection)?;
448    let persistence =
449        persist_reactive_list_with_cursor(&collection, options.persistence, initial_cursor)?;
450    Ok(OpenPersistentReactiveList {
451        collection,
452        persistence,
453    })
454}
455
456/// Creates or computes `open_persistent_reactive_log`.
457pub fn open_persistent_reactive_log<T>(
458    options: OpenPersistentReactiveLogOptions<T>,
459) -> StorageResult<OpenPersistentReactiveLog<T>>
460where
461    T: Clone + Serialize + DeserializeOwned + 'static,
462{
463    let mut state = load_reactive_log_state(
464        options.persistence.snapshot_store.as_ref(),
465        LoadReactiveCollectionStateOptions {
466            storage_prefix: options.persistence.storage_prefix.as_deref(),
467            snapshot_key: options.persistence.snapshot_key.as_deref(),
468            change_log: options.persistence.change_log.as_deref(),
469        },
470    )?;
471    if !state.snapshot_found && state.changes_applied == 0 {
472        state.state = options.initial;
473    }
474    let initial_cursor = state.cursor;
475    let collection = restore_reactive_log(state, options.collection)?;
476    let persistence =
477        persist_reactive_log_with_cursor(&collection, options.persistence, initial_cursor)?;
478    Ok(OpenPersistentReactiveLog {
479        collection,
480        persistence,
481    })
482}
483
484/// Creates or computes `open_persistent_reactive_map`.
485pub fn open_persistent_reactive_map<K, V>(
486    options: OpenPersistentReactiveMapOptions<K, V>,
487) -> StorageResult<OpenPersistentReactiveMap<K, V>>
488where
489    K: Clone + Ord + std::fmt::Debug + Serialize + DeserializeOwned + 'static,
490    V: Clone + Serialize + DeserializeOwned + 'static,
491{
492    let mut state = load_reactive_map_state(
493        options.persistence.snapshot_store.as_ref(),
494        LoadReactiveCollectionStateOptions {
495            storage_prefix: options.persistence.storage_prefix.as_deref(),
496            snapshot_key: options.persistence.snapshot_key.as_deref(),
497            change_log: options.persistence.change_log.as_deref(),
498        },
499    )?;
500    if matches!(
501        state.source,
502        crate::storage::ReactiveCollectionRestoreSource::Empty
503    ) {
504        state.state = options.initial;
505    }
506    let initial_cursor = state.cursor;
507    let collection = restore_reactive_map(state, options.collection)?;
508    let persistence =
509        persist_reactive_map_with_cursor(&collection, options.persistence, initial_cursor)?;
510    Ok(OpenPersistentReactiveMap {
511        collection,
512        persistence,
513    })
514}
515
516/// Creates or computes `open_persistent_reactive_index`.
517pub fn open_persistent_reactive_index<K, S, V>(
518    options: OpenPersistentReactiveIndexOptions<K, S, V>,
519) -> StorageResult<OpenPersistentReactiveIndex<K, S, V>>
520where
521    K: Clone + Ord + std::fmt::Debug + Serialize + DeserializeOwned + 'static,
522    S: Clone + Ord + Serialize + DeserializeOwned + 'static,
523    V: Clone + Serialize + DeserializeOwned + 'static,
524{
525    let mut state = load_reactive_index_state(
526        options.persistence.snapshot_store.as_ref(),
527        LoadReactiveCollectionStateOptions {
528            storage_prefix: options.persistence.storage_prefix.as_deref(),
529            snapshot_key: options.persistence.snapshot_key.as_deref(),
530            change_log: options.persistence.change_log.as_deref(),
531        },
532    )?;
533    if matches!(
534        state.source,
535        crate::storage::ReactiveCollectionRestoreSource::Empty
536    ) {
537        state.state = options.initial;
538    }
539    let initial_cursor = state.cursor;
540    let collection = restore_reactive_index(state, options.collection)?;
541    let persistence =
542        persist_reactive_index_with_cursor(&collection, options.persistence, initial_cursor)?;
543    Ok(OpenPersistentReactiveIndex {
544        collection,
545        persistence,
546    })
547}
548
549fn persist_collection<C, S, F, A>(
550    kind: ReactiveCollectionKind,
551    snapshot_state: S,
552    delta: &Node<C>,
553    options: PersistReactiveCollectionOptions,
554    initial_cursor: Option<u64>,
555    encode_change: F,
556    apply_change: A,
557) -> StorageResult<ReactiveCollectionPersistence>
558where
559    C: Clone + 'static,
560    S: Serialize + Clone + 'static,
561    F: Fn(&C) -> StorageResult<Value> + 'static,
562    A: Fn(&mut S, &C) -> StorageResult<()> + 'static,
563{
564    let snapshot_key = resolve_snapshot_key(&options)?;
565    if let Some(every) = options.snapshot_every_changes {
566        if every == 0 {
567            return Err(StorageError::backend(
568                "persistReactiveCollection: snapshot_every_changes must be positive",
569            ));
570        }
571    }
572    let graph = options.graph.as_ref().ok_or_else(|| {
573        StorageError::backend(
574            "persistReactiveCollection: graph is required for graph-visible sidecar facts",
575        )
576    })?;
577    if !graph.contains_core(&delta.erased()) {
578        return Err(StorageError::backend(
579            "persistReactiveCollection: collection delta belongs to a different graph or is not graph-registered",
580        ));
581    }
582    let fact_prefix = options
583        .name
584        .clone()
585        .or_else(|| options.storage_prefix.clone())
586        .unwrap_or_else(|| "reactiveCollection.persistence".to_owned());
587    let ready = persistence_fact(graph, format!("{fact_prefix}.ready"), false);
588    let status = persistence_fact(
589        graph,
590        format!("{fact_prefix}.status"),
591        status_json(
592            ReactiveCollectionPersistenceStatus::Starting,
593            0,
594            0,
595            cursor_json(kind, initial_cursor, 0, 0),
596        ),
597    );
598    let error = persistence_fact(graph, format!("{fact_prefix}.error"), Value::Null);
599    let cursor = persistence_fact(
600        graph,
601        format!("{fact_prefix}.cursor"),
602        cursor_json(kind, initial_cursor, 0, 0),
603    );
604
605    let disposed = Rc::new(Cell::new(false));
606    let errored = Rc::new(Cell::new(false));
607    let cursor_cell = Rc::new(Cell::new(initial_cursor));
608    let snapshot_writes = Rc::new(Cell::new(0usize));
609    let change_writes = Rc::new(Cell::new(0usize));
610    let error_count = Rc::new(Cell::new(0usize));
611    let snapshot_state = Rc::new(RefCell::new(snapshot_state));
612    let pending_changes = Rc::new(RefCell::new(Vec::<ReactiveCollectionChangeFrame>::new()));
613    let snapshot_dirty = Rc::new(Cell::new(false));
614    let cadence_snapshot_due = Rc::new(Cell::new(false));
615    let snapshot_store = options.snapshot_store.clone();
616    let change_log = options.change_log.clone();
617    let encode_change = Rc::new(encode_change);
618    let apply_change = Rc::new(apply_change);
619    let snapshot_every_changes = options.snapshot_every_changes;
620    let changes_since_snapshot = Rc::new(Cell::new(0usize));
621
622    let write_snapshot = {
623        let ready = ready.clone();
624        let status = status.clone();
625        let error = error.clone();
626        let cursor = cursor.clone();
627        let snapshot_key = snapshot_key.clone();
628        let snapshot_store = snapshot_store.clone();
629        let snapshot_state = snapshot_state.clone();
630        let cursor_cell = cursor_cell.clone();
631        let snapshot_writes = snapshot_writes.clone();
632        let change_writes = change_writes.clone();
633        let error_count = error_count.clone();
634        let pending_changes = pending_changes.clone();
635        let snapshot_dirty = snapshot_dirty.clone();
636        let cadence_snapshot_due = cadence_snapshot_due.clone();
637        let change_log = change_log.clone();
638        let errored = errored.clone();
639        move || {
640            drain_pending_changes(
641                pending_changes.clone(),
642                change_log.clone(),
643                cursor_cell.clone(),
644                cursor.clone(),
645                kind,
646                snapshot_writes.clone(),
647                change_writes.clone(),
648            )?;
649            let cursor_value = cursor_cell
650                .get()
651                .map(seq_to_snapshot_cursor)
652                .transpose()?
653                .unwrap_or(-1);
654            let snapshot_value = serde_json::to_value(snapshot_state.borrow().clone())
655                .map_err(storage_json_error)?;
656            let frame = reactive_collection_snapshot_frame(kind, cursor_value, snapshot_value)?;
657            snapshot_store.set(&snapshot_key, frame)?;
658            snapshot_writes.set(snapshot_writes.get().saturating_add(1));
659            let cursor_value = cursor_json(
660                kind,
661                cursor_cell.get(),
662                snapshot_writes.get(),
663                change_writes.get(),
664            );
665            if !errored.get() {
666                ready.set(true);
667                snapshot_dirty.set(false);
668                cadence_snapshot_due.set(false);
669                ready.set(true);
670                status.set(status_json(
671                    ReactiveCollectionPersistenceStatus::Ready,
672                    pending_changes.borrow().len(),
673                    error_count.get(),
674                    cursor_value.clone(),
675                ));
676                error.set(Value::Null);
677                cursor.set(cursor_value);
678            }
679            Ok(())
680        }
681    };
682
683    if options.snapshot_on_attach {
684        if let Err(err) = write_snapshot() {
685            record_persistence_error(
686                PersistenceErrorTargets {
687                    ready: &ready,
688                    status: &status,
689                    error: &error,
690                    errored: &errored,
691                    error_count: &error_count,
692                },
693                PersistenceErrorFacts {
694                    kind,
695                    cursor: cursor_cell.get(),
696                    snapshot_writes: snapshot_writes.get(),
697                    change_writes: change_writes.get(),
698                    pending: pending_changes.borrow().len(),
699                },
700                "snapshot",
701                err,
702            );
703        }
704    } else {
705        ready.set(true);
706        status.set(status_json(
707            ReactiveCollectionPersistenceStatus::Ready,
708            pending_changes.borrow().len(),
709            error_count.get(),
710            cursor_json(
711                kind,
712                cursor_cell.get(),
713                snapshot_writes.get(),
714                change_writes.get(),
715            ),
716        ));
717    }
718
719    let write_snapshot_for_control = Rc::new(write_snapshot);
720    let flush = {
721        let disposed = disposed.clone();
722        let errored = errored.clone();
723        let ready = ready.clone();
724        let status = status.clone();
725        let error = error.clone();
726        let cursor = cursor.clone();
727        let cursor_cell = cursor_cell.clone();
728        let snapshot_writes = snapshot_writes.clone();
729        let change_writes = change_writes.clone();
730        let error_count = error_count.clone();
731        let pending_changes = pending_changes.clone();
732        let change_log = change_log.clone();
733        let snapshot_dirty = snapshot_dirty.clone();
734        let cadence_snapshot_due = cadence_snapshot_due.clone();
735        let write_snapshot = write_snapshot_for_control.clone();
736        move || {
737            if disposed.get() {
738                return Ok(());
739            }
740            if errored.get() {
741                return Err(StorageError::backend(
742                    "reactiveCollection persistence is errored; inspect error fact",
743                ));
744            }
745            let writes_snapshot =
746                (change_log.is_none() && snapshot_dirty.get()) || cadence_snapshot_due.get();
747            let result = if writes_snapshot {
748                write_snapshot()
749            } else {
750                match drain_pending_changes(
751                    pending_changes.clone(),
752                    change_log.clone(),
753                    cursor_cell.clone(),
754                    cursor.clone(),
755                    kind,
756                    snapshot_writes.clone(),
757                    change_writes.clone(),
758                ) {
759                    Ok(()) => {
760                        ready.set(true);
761                        status.set(status_json(
762                            ReactiveCollectionPersistenceStatus::Ready,
763                            pending_changes.borrow().len(),
764                            error_count.get(),
765                            cursor_json(
766                                kind,
767                                cursor_cell.get(),
768                                snapshot_writes.get(),
769                                change_writes.get(),
770                            ),
771                        ));
772                        error.set(Value::Null);
773                        Ok(())
774                    }
775                    Err(err) => Err(err),
776                }
777            };
778            if let Err(err) = result {
779                record_persistence_error(
780                    PersistenceErrorTargets {
781                        ready: &ready,
782                        status: &status,
783                        error: &error,
784                        errored: &errored,
785                        error_count: &error_count,
786                    },
787                    PersistenceErrorFacts {
788                        kind,
789                        cursor: cursor_cell.get(),
790                        snapshot_writes: snapshot_writes.get(),
791                        change_writes: change_writes.get(),
792                        pending: pending_changes.borrow().len(),
793                    },
794                    if writes_snapshot {
795                        "snapshot"
796                    } else {
797                        "change"
798                    },
799                    err.clone(),
800                );
801                return Err(err);
802            }
803            Ok(())
804        }
805    };
806    let snapshot = {
807        let disposed = disposed.clone();
808        let write_snapshot = write_snapshot_for_control.clone();
809        let ready = ready.clone();
810        let status = status.clone();
811        let error = error.clone();
812        let errored = errored.clone();
813        let cursor_cell = cursor_cell.clone();
814        let snapshot_writes = snapshot_writes.clone();
815        let change_writes = change_writes.clone();
816        let error_count = error_count.clone();
817        let pending_changes = pending_changes.clone();
818        move || {
819            if disposed.get() {
820                return Err(StorageError::backend(
821                    "reactiveCollection persistence is disposed",
822                ));
823            }
824            match write_snapshot() {
825                Ok(()) => Ok(()),
826                Err(err) => {
827                    record_persistence_error(
828                        PersistenceErrorTargets {
829                            ready: &ready,
830                            status: &status,
831                            error: &error,
832                            errored: &errored,
833                            error_count: &error_count,
834                        },
835                        PersistenceErrorFacts {
836                            kind,
837                            cursor: cursor_cell.get(),
838                            snapshot_writes: snapshot_writes.get(),
839                            change_writes: change_writes.get(),
840                            pending: pending_changes.borrow().len(),
841                        },
842                        "snapshot",
843                        err.clone(),
844                    );
845                    Err(err)
846                }
847            }
848        }
849    };
850
851    let ready_for_sub = ready.clone();
852    let status_for_sub = status.clone();
853    let error_for_sub = error.clone();
854    let snapshot_state_for_sub = snapshot_state.clone();
855    let disposed_for_sub = disposed.clone();
856    let errored_for_sub = errored.clone();
857    let cursor_cell_for_sub = cursor_cell.clone();
858    let snapshot_writes_for_sub = snapshot_writes.clone();
859    let change_writes_for_sub = change_writes.clone();
860    let error_count_for_sub = error_count.clone();
861    let change_log_for_sub = change_log.clone();
862    let apply_change_for_sub = apply_change.clone();
863    let pending_changes_for_sub = pending_changes.clone();
864    let changes_since_snapshot_for_sub = changes_since_snapshot.clone();
865    let snapshot_dirty_for_sub = snapshot_dirty.clone();
866    let cadence_snapshot_due_for_sub = cadence_snapshot_due.clone();
867    let armed = Rc::new(Cell::new(false));
868    let armed_for_sub = armed.clone();
869    let unsub = delta.subscribe(move |message| {
870        if disposed_for_sub.get() || errored_for_sub.get() {
871            return;
872        }
873        let Message::Data(value) = message else {
874            return;
875        };
876        if !armed_for_sub.get() {
877            return;
878        }
879        let Some(change) = value.as_ref().downcast_ref::<C>() else {
880            return;
881        };
882        ready_for_sub.set(false);
883        status_for_sub.set(status_json(
884            ReactiveCollectionPersistenceStatus::Flushing,
885            pending_changes_for_sub.borrow().len(),
886            error_count_for_sub.get(),
887            cursor_json(
888                kind,
889                cursor_cell_for_sub.get(),
890                snapshot_writes_for_sub.get(),
891                change_writes_for_sub.get(),
892            ),
893        ));
894        let result = (|| {
895            apply_change_for_sub(&mut snapshot_state_for_sub.borrow_mut(), change)?;
896            let change_value = encode_change(change)?;
897            let frame = reactive_collection_change_frame(kind, change_value)?;
898            if change_log_for_sub.is_some() {
899                pending_changes_for_sub.borrow_mut().push(frame);
900            } else {
901                snapshot_dirty_for_sub.set(true);
902            }
903            Ok(())
904        })();
905        match result {
906            Ok(()) => {
907                error_for_sub.set(Value::Null);
908                let next = changes_since_snapshot_for_sub.get().saturating_add(1);
909                changes_since_snapshot_for_sub.set(next);
910                if snapshot_every_changes.is_some_and(|every| next >= every) {
911                    changes_since_snapshot_for_sub.set(0);
912                    cadence_snapshot_due_for_sub.set(true);
913                }
914            }
915            Err(err) => {
916                record_persistence_error(
917                    PersistenceErrorTargets {
918                        ready: &ready_for_sub,
919                        status: &status_for_sub,
920                        error: &error_for_sub,
921                        errored: &errored_for_sub,
922                        error_count: &error_count_for_sub,
923                    },
924                    PersistenceErrorFacts {
925                        kind,
926                        cursor: cursor_cell_for_sub.get(),
927                        snapshot_writes: snapshot_writes_for_sub.get(),
928                        change_writes: change_writes_for_sub.get(),
929                        pending: pending_changes_for_sub.borrow().len(),
930                    },
931                    "change",
932                    err,
933                );
934            }
935        }
936    });
937    armed.set(true);
938
939    let dispose = {
940        let ready = ready.clone();
941        let status = status.clone();
942        let disposed = disposed.clone();
943        let cursor_cell = cursor_cell.clone();
944        let snapshot_writes = snapshot_writes.clone();
945        let change_writes = change_writes.clone();
946        let error_count = error_count.clone();
947        let pending_changes = pending_changes.clone();
948        let cadence_snapshot_due = cadence_snapshot_due.clone();
949        Box::new(move || {
950            disposed.set(true);
951            pending_changes.borrow_mut().clear();
952            cadence_snapshot_due.set(false);
953            ready.set(false);
954            status.set(status_json(
955                ReactiveCollectionPersistenceStatus::Disposed,
956                pending_changes.borrow().len(),
957                error_count.get(),
958                cursor_json(
959                    kind,
960                    cursor_cell.get(),
961                    snapshot_writes.get(),
962                    change_writes.get(),
963                ),
964            ));
965            unsub();
966        }) as Disposer
967    };
968
969    Ok(ReactiveCollectionPersistence {
970        ready,
971        status,
972        error,
973        cursor,
974        flush: Rc::new(flush),
975        snapshot: Rc::new(snapshot),
976        dispose: RefCell::new(Some(dispose)),
977    })
978}
979
980fn drain_pending_changes(
981    pending_changes: Rc<RefCell<Vec<ReactiveCollectionChangeFrame>>>,
982    change_log: Option<Rc<dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>>,
983    cursor_cell: Rc<Cell<Option<u64>>>,
984    cursor: Node<Value>,
985    kind: ReactiveCollectionKind,
986    snapshot_writes: Rc<Cell<usize>>,
987    change_writes: Rc<Cell<usize>>,
988) -> StorageResult<()> {
989    let Some(log) = change_log else {
990        pending_changes.borrow_mut().clear();
991        return Ok(());
992    };
993    loop {
994        let frame = { pending_changes.borrow().first().cloned() };
995        let Some(frame) = frame else {
996            break;
997        };
998        let entry = log.append(frame)?;
999        cursor_cell.set(Some(entry.seq));
1000        change_writes.set(change_writes.get().saturating_add(1));
1001        cursor.set(cursor_json(
1002            kind,
1003            Some(entry.seq),
1004            snapshot_writes.get(),
1005            change_writes.get(),
1006        ));
1007        pending_changes.borrow_mut().remove(0);
1008    }
1009    Ok(())
1010}
1011
1012fn persistence_fact<T: 'static>(graph: &Graph, name: String, initial: T) -> Node<T> {
1013    graph.state_opts(initial, GraphNodeOpts::named(name))
1014}
1015
1016fn resolve_snapshot_key(options: &PersistReactiveCollectionOptions) -> StorageResult<String> {
1017    if let Some(key) = options.snapshot_key.as_ref() {
1018        if key.is_empty() {
1019            return Err(StorageError::backend(
1020                "persistReactiveCollection: snapshot_key must be non-empty",
1021            ));
1022        }
1023        return Ok(key.clone());
1024    }
1025    let prefix = options.storage_prefix.as_ref().ok_or_else(|| {
1026        StorageError::backend(
1027            "persistReactiveCollection: storage_prefix or snapshot_key is required",
1028        )
1029    })?;
1030    reactive_collection_snapshot_key(prefix)
1031}
1032
1033fn seq_to_snapshot_cursor(seq: u64) -> StorageResult<i64> {
1034    i64::try_from(seq)
1035        .map_err(|_| StorageError::backend("reactiveCollection cursor exceeds i64 range"))
1036}
1037
1038fn storage_json_error(error: serde_json::Error) -> StorageError {
1039    StorageError::backend(format!("reactiveCollection JSON error: {error}"))
1040}
1041
1042struct PersistenceErrorTargets<'a> {
1043    ready: &'a Node<bool>,
1044    status: &'a Node<Value>,
1045    error: &'a Node<Value>,
1046    errored: &'a Cell<bool>,
1047    error_count: &'a Cell<usize>,
1048}
1049
1050struct PersistenceErrorFacts {
1051    kind: ReactiveCollectionKind,
1052    cursor: Option<u64>,
1053    snapshot_writes: usize,
1054    change_writes: usize,
1055    pending: usize,
1056}
1057
1058fn record_persistence_error(
1059    targets: PersistenceErrorTargets<'_>,
1060    facts: PersistenceErrorFacts,
1061    phase: &str,
1062    err: StorageError,
1063) {
1064    targets.errored.set(true);
1065    targets
1066        .error_count
1067        .set(targets.error_count.get().saturating_add(1));
1068    let cursor = cursor_json(
1069        facts.kind,
1070        facts.cursor,
1071        facts.snapshot_writes,
1072        facts.change_writes,
1073    );
1074    targets.ready.set(false);
1075    targets.status.set(status_json(
1076        ReactiveCollectionPersistenceStatus::Errored,
1077        facts.pending,
1078        targets.error_count.get(),
1079        cursor.clone(),
1080    ));
1081    targets.error.set(json!({
1082        "phase": phase,
1083        "message": err.to_string(),
1084        "cursor": cursor
1085    }));
1086}
1087
1088fn cursor_json(
1089    collection: ReactiveCollectionKind,
1090    cursor: Option<u64>,
1091    snapshot_writes: usize,
1092    change_writes: usize,
1093) -> Value {
1094    json!({
1095        "kind": "persistence.cursor",
1096        "collection": collection_name(collection),
1097        "changeSeq": cursor.map_or(-1_i64, |seq| i64::try_from(seq).unwrap_or(i64::MAX)),
1098        "snapshotWrites": snapshot_writes,
1099        "changeWrites": change_writes
1100    })
1101}
1102
1103fn status_json(
1104    state: ReactiveCollectionPersistenceStatus,
1105    pending: usize,
1106    errors: usize,
1107    cursor: Value,
1108) -> Value {
1109    json!({
1110        "state": status_name(&state),
1111        "pending": pending,
1112        "writes": cursor
1113            .get("snapshotWrites")
1114            .and_then(Value::as_u64)
1115            .unwrap_or(0)
1116            .saturating_add(cursor.get("changeWrites").and_then(Value::as_u64).unwrap_or(0)),
1117        "errors": errors,
1118        "cursor": cursor
1119    })
1120}
1121
1122fn collection_name(kind: ReactiveCollectionKind) -> &'static str {
1123    match kind {
1124        ReactiveCollectionKind::ReactiveList => "reactiveList",
1125        ReactiveCollectionKind::ReactiveLog => "reactiveLog",
1126        ReactiveCollectionKind::ReactiveMap => "reactiveMap",
1127        ReactiveCollectionKind::ReactiveIndex => "reactiveIndex",
1128    }
1129}
1130
1131fn status_name(status: &ReactiveCollectionPersistenceStatus) -> &'static str {
1132    match status {
1133        ReactiveCollectionPersistenceStatus::Starting => "starting",
1134        ReactiveCollectionPersistenceStatus::Ready => "ready",
1135        ReactiveCollectionPersistenceStatus::Flushing => "flushing",
1136        ReactiveCollectionPersistenceStatus::Errored => "errored",
1137        ReactiveCollectionPersistenceStatus::Disposed => "disposed",
1138    }
1139}
1140
1141fn parse_status_fact(value: &Value) -> StorageResult<ReactiveCollectionPersistenceStatusFact> {
1142    let object = value
1143        .as_object()
1144        .ok_or_else(|| StorageError::backend("reactiveCollection status fact must be an object"))?;
1145    let state = parse_status(
1146        object
1147            .get("state")
1148            .and_then(Value::as_str)
1149            .ok_or_else(|| StorageError::backend("reactiveCollection status.state is missing"))?,
1150    )?;
1151    let pending = parse_usize_field(object.get("pending"), "reactiveCollection status.pending")?;
1152    let writes = parse_usize_field(object.get("writes"), "reactiveCollection status.writes")?;
1153    let errors = parse_usize_field(object.get("errors"), "reactiveCollection status.errors")?;
1154    let cursor =
1155        parse_cursor_fact(object.get("cursor").ok_or_else(|| {
1156            StorageError::backend("reactiveCollection status.cursor is missing")
1157        })?)?;
1158    Ok(ReactiveCollectionPersistenceStatusFact {
1159        state,
1160        pending,
1161        writes,
1162        errors,
1163        cursor,
1164    })
1165}
1166
1167fn parse_error_fact(value: &Value) -> StorageResult<ReactiveCollectionPersistenceErrorFact> {
1168    let object = value
1169        .as_object()
1170        .ok_or_else(|| StorageError::backend("reactiveCollection error fact must be an object"))?;
1171    let phase = object
1172        .get("phase")
1173        .and_then(Value::as_str)
1174        .ok_or_else(|| StorageError::backend("reactiveCollection error.phase is missing"))?
1175        .to_owned();
1176    let message = object
1177        .get("message")
1178        .and_then(Value::as_str)
1179        .ok_or_else(|| StorageError::backend("reactiveCollection error.message is missing"))?
1180        .to_owned();
1181    let cursor = parse_cursor_fact(
1182        object
1183            .get("cursor")
1184            .ok_or_else(|| StorageError::backend("reactiveCollection error.cursor is missing"))?,
1185    )?;
1186    Ok(ReactiveCollectionPersistenceErrorFact {
1187        phase,
1188        message,
1189        cursor,
1190    })
1191}
1192
1193fn parse_cursor_fact(value: &Value) -> StorageResult<ReactiveCollectionPersistenceCursor> {
1194    let object = value
1195        .as_object()
1196        .ok_or_else(|| StorageError::backend("reactiveCollection cursor fact must be an object"))?;
1197    if object.get("kind").and_then(Value::as_str) != Some("persistence.cursor") {
1198        return Err(StorageError::backend(
1199            "reactiveCollection cursor.kind must be persistence.cursor",
1200        ));
1201    }
1202    let collection = parse_collection_kind(
1203        object
1204            .get("collection")
1205            .and_then(Value::as_str)
1206            .ok_or_else(|| {
1207                StorageError::backend("reactiveCollection cursor.collection is missing")
1208            })?,
1209    )?;
1210    let change_seq = object
1211        .get("changeSeq")
1212        .and_then(Value::as_i64)
1213        .ok_or_else(|| StorageError::backend("reactiveCollection cursor.changeSeq is missing"))?;
1214    let change_seq = if change_seq < 0 {
1215        None
1216    } else {
1217        Some(change_seq as u64)
1218    };
1219    Ok(ReactiveCollectionPersistenceCursor {
1220        collection,
1221        change_seq,
1222        snapshot_writes: parse_usize_field(
1223            object.get("snapshotWrites"),
1224            "reactiveCollection cursor.snapshotWrites",
1225        )?,
1226        change_writes: parse_usize_field(
1227            object.get("changeWrites"),
1228            "reactiveCollection cursor.changeWrites",
1229        )?,
1230    })
1231}
1232
1233fn parse_status(value: &str) -> StorageResult<ReactiveCollectionPersistenceStatus> {
1234    match value {
1235        "starting" => Ok(ReactiveCollectionPersistenceStatus::Starting),
1236        "ready" => Ok(ReactiveCollectionPersistenceStatus::Ready),
1237        "flushing" => Ok(ReactiveCollectionPersistenceStatus::Flushing),
1238        "errored" => Ok(ReactiveCollectionPersistenceStatus::Errored),
1239        "disposed" => Ok(ReactiveCollectionPersistenceStatus::Disposed),
1240        _ => Err(StorageError::backend(format!(
1241            "reactiveCollection status.state is unsupported: {value}"
1242        ))),
1243    }
1244}
1245
1246fn parse_collection_kind(value: &str) -> StorageResult<ReactiveCollectionKind> {
1247    match value {
1248        "reactiveList" => Ok(ReactiveCollectionKind::ReactiveList),
1249        "reactiveLog" => Ok(ReactiveCollectionKind::ReactiveLog),
1250        "reactiveMap" => Ok(ReactiveCollectionKind::ReactiveMap),
1251        "reactiveIndex" => Ok(ReactiveCollectionKind::ReactiveIndex),
1252        _ => Err(StorageError::backend(format!(
1253            "reactiveCollection cursor.collection is unsupported: {value}"
1254        ))),
1255    }
1256}
1257
1258fn parse_usize_field(value: Option<&Value>, label: &str) -> StorageResult<usize> {
1259    let value = value
1260        .and_then(Value::as_u64)
1261        .ok_or_else(|| StorageError::backend(format!("{label} must be a non-negative integer")))?;
1262    usize::try_from(value).map_err(|_| StorageError::backend(format!("{label} exceeds usize")))
1263}
1264
1265fn apply_list_snapshot_change<T>(snapshot: &mut Vec<T>, change: &ListChange<T>) -> StorageResult<()>
1266where
1267    T: Clone + Serialize,
1268{
1269    match change {
1270        ListChange::Append { value } => snapshot.push(value.clone()),
1271        ListChange::AppendMany { values } => snapshot.extend(values.clone()),
1272        ListChange::Insert { index, value } => {
1273            if *index > snapshot.len() {
1274                return Err(StorageError::backend(
1275                    "reactiveList persistence mirror: insert index out of bounds",
1276                ));
1277            }
1278            snapshot.insert(*index, value.clone());
1279        }
1280        ListChange::InsertMany { index, values } => {
1281            if *index > snapshot.len() {
1282                return Err(StorageError::backend(
1283                    "reactiveList persistence mirror: insertMany index out of bounds",
1284                ));
1285            }
1286            snapshot.splice(*index..*index, values.clone());
1287        }
1288        ListChange::Pop { index, value } => {
1289            if *index >= snapshot.len() {
1290                return Err(StorageError::backend(
1291                    "reactiveList persistence mirror: pop index out of bounds",
1292                ));
1293            }
1294            let actual = snapshot.remove(*index);
1295            if !strict_json_equal(&actual, value)? {
1296                return Err(StorageError::backend(
1297                    "reactiveList persistence mirror: pop value does not match stored state",
1298                ));
1299            }
1300        }
1301        ListChange::TrimHead { n } => {
1302            if *n > snapshot.len() {
1303                return Err(StorageError::backend(
1304                    "reactiveList persistence mirror: trimHead out of bounds",
1305                ));
1306            }
1307            snapshot.drain(0..*n);
1308        }
1309        ListChange::Clear { count } => {
1310            if *count != snapshot.len() {
1311                return Err(StorageError::backend(
1312                    "reactiveList persistence mirror: clear count does not match stored state",
1313                ));
1314            }
1315            snapshot.clear();
1316        }
1317    }
1318    Ok(())
1319}
1320
1321fn apply_log_snapshot_change<T>(snapshot: &mut Vec<T>, change: &LogChange<T>) -> StorageResult<()>
1322where
1323    T: Clone,
1324{
1325    match change {
1326        LogChange::Append { value } => snapshot.push(value.clone()),
1327        LogChange::AppendMany { values } => snapshot.extend(values.clone()),
1328        LogChange::TrimHead { n } => {
1329            if *n > snapshot.len() {
1330                return Err(StorageError::backend(
1331                    "reactiveLog persistence mirror: trimHead out of bounds",
1332                ));
1333            }
1334            snapshot.drain(0..*n);
1335        }
1336        LogChange::Clear { count } => {
1337            if *count != snapshot.len() {
1338                return Err(StorageError::backend(
1339                    "reactiveLog persistence mirror: clear count does not match stored state",
1340                ));
1341            }
1342            snapshot.clear();
1343        }
1344    }
1345    Ok(())
1346}
1347
1348fn apply_map_snapshot_change<K, V>(
1349    snapshot: &mut Vec<(K, V)>,
1350    change: &MapChange<K, V>,
1351) -> StorageResult<()>
1352where
1353    K: Clone + Serialize,
1354    V: Clone + Serialize,
1355{
1356    match change {
1357        MapChange::Set { key, value } => match find_map_key(snapshot, key)? {
1358            Some(index) => snapshot[index] = (key.clone(), value.clone()),
1359            None => snapshot.push((key.clone(), value.clone())),
1360        },
1361        MapChange::Delete { key, previous } => {
1362            let Some(index) = find_map_key(snapshot, key)? else {
1363                return Err(StorageError::backend(
1364                    "reactiveMap persistence mirror: delete key is missing",
1365                ));
1366            };
1367            if !strict_json_equal(&snapshot[index].1, previous)? {
1368                return Err(StorageError::backend(
1369                    "reactiveMap persistence mirror: delete previous value does not match stored state",
1370                ));
1371            }
1372            snapshot.remove(index);
1373        }
1374        MapChange::Clear { count } => {
1375            if *count != snapshot.len() {
1376                return Err(StorageError::backend(
1377                    "reactiveMap persistence mirror: clear count does not match stored state",
1378                ));
1379            }
1380            snapshot.clear();
1381        }
1382    }
1383    Ok(())
1384}
1385
1386fn apply_index_snapshot_change<K, S, V>(
1387    snapshot: &mut Vec<IndexRow<K, S, V>>,
1388    change: &IndexChange<K, S, V>,
1389) -> StorageResult<()>
1390where
1391    K: Clone + Serialize,
1392    S: Clone + Serialize,
1393    V: Clone + Serialize,
1394{
1395    match change {
1396        IndexChange::Upsert {
1397            primary,
1398            secondary,
1399            value,
1400        } => {
1401            let row = IndexRow {
1402                primary: primary.clone(),
1403                secondary: secondary.clone(),
1404                value: value.clone(),
1405            };
1406            match find_index_primary(snapshot, primary)? {
1407                Some(index) => snapshot[index] = row,
1408                None => snapshot.push(row),
1409            }
1410        }
1411        IndexChange::Delete { primary } => {
1412            let Some(index) = find_index_primary(snapshot, primary)? else {
1413                return Err(StorageError::backend(
1414                    "reactiveIndex persistence mirror: delete primary is missing",
1415                ));
1416            };
1417            snapshot.remove(index);
1418        }
1419        IndexChange::DeleteMany { primaries } => {
1420            remove_index_primaries(
1421                snapshot,
1422                primaries,
1423                "reactiveIndex persistence mirror: deleteMany primary",
1424            )?;
1425        }
1426        IndexChange::Clear { count } => {
1427            if *count != snapshot.len() {
1428                return Err(StorageError::backend(
1429                    "reactiveIndex persistence mirror: clear count does not match stored state",
1430                ));
1431            }
1432            snapshot.clear();
1433        }
1434    }
1435    Ok(())
1436}
1437
1438fn find_map_key<K, V>(entries: &[(K, V)], key: &K) -> StorageResult<Option<usize>>
1439where
1440    K: Serialize,
1441{
1442    let target = strict_json_identity(key)?;
1443    for (index, (candidate, _)) in entries.iter().enumerate() {
1444        if strict_json_identity(candidate)? == target {
1445            return Ok(Some(index));
1446        }
1447    }
1448    Ok(None)
1449}
1450
1451fn find_index_primary<K, S, V>(
1452    rows: &[IndexRow<K, S, V>],
1453    primary: &K,
1454) -> StorageResult<Option<usize>>
1455where
1456    K: Serialize,
1457{
1458    let target = strict_json_identity(primary)?;
1459    for (index, row) in rows.iter().enumerate() {
1460        if strict_json_identity(&row.primary)? == target {
1461            return Ok(Some(index));
1462        }
1463    }
1464    Ok(None)
1465}
1466
1467fn remove_index_primaries<K, S, V>(
1468    rows: &mut Vec<IndexRow<K, S, V>>,
1469    primaries: &[K],
1470    label: &str,
1471) -> StorageResult<()>
1472where
1473    K: Serialize,
1474{
1475    let mut seen = Vec::<Vec<u8>>::new();
1476    let mut indexes = Vec::<usize>::new();
1477    for (index, primary) in primaries.iter().enumerate() {
1478        let id = strict_json_identity(primary)?;
1479        if seen.iter().any(|existing| existing == &id) {
1480            return Err(StorageError::backend(format!(
1481                "{label} {index} duplicates an earlier primary"
1482            )));
1483        }
1484        seen.push(id);
1485        let Some(row_index) = find_index_primary(rows, primary)? else {
1486            return Err(StorageError::backend(format!("{label} {index} is missing")));
1487        };
1488        indexes.push(row_index);
1489    }
1490    indexes.sort_unstable_by(|a, b| b.cmp(a));
1491    for index in indexes {
1492        rows.remove(index);
1493    }
1494    Ok(())
1495}
1496
1497fn strict_json_equal<T: Serialize>(left: &T, right: &T) -> StorageResult<bool> {
1498    let left = serde_json::to_value(left).map_err(storage_json_error)?;
1499    let right = serde_json::to_value(right).map_err(storage_json_error)?;
1500    let left = crate::strict_canonical_json_bytes(&left)
1501        .map_err(|err| StorageError::backend(format!("reactiveCollection JSON error: {err}")))?;
1502    let right = crate::strict_canonical_json_bytes(&right)
1503        .map_err(|err| StorageError::backend(format!("reactiveCollection JSON error: {err}")))?;
1504    Ok(left == right)
1505}
1506
1507fn strict_json_identity<T: Serialize>(value: &T) -> StorageResult<Vec<u8>> {
1508    let value = serde_json::to_value(value).map_err(storage_json_error)?;
1509    crate::strict_canonical_json_bytes(&value)
1510        .map_err(|err| StorageError::backend(format!("reactiveCollection JSON error: {err}")))
1511}