1use 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)]
34pub enum ReactiveCollectionPersistenceStatus {
36 Starting,
38 Ready,
40 Flushing,
42 Errored,
44 Disposed,
46}
47
48#[derive(Clone, Debug, Eq, PartialEq)]
49pub struct ReactiveCollectionPersistenceCursor {
51 pub collection: ReactiveCollectionKind,
53 pub change_seq: Option<u64>,
55 pub snapshot_writes: usize,
57 pub change_writes: usize,
59}
60
61#[derive(Clone, Debug, Eq, PartialEq)]
62pub struct ReactiveCollectionPersistenceStatusFact {
64 pub state: ReactiveCollectionPersistenceStatus,
66 pub pending: usize,
68 pub writes: usize,
70 pub errors: usize,
72 pub cursor: ReactiveCollectionPersistenceCursor,
74}
75
76#[derive(Clone, Debug, Eq, PartialEq)]
77pub struct ReactiveCollectionPersistenceErrorFact {
79 pub phase: String,
81 pub message: String,
83 pub cursor: ReactiveCollectionPersistenceCursor,
85}
86
87#[derive(Clone)]
88pub struct PersistReactiveCollectionOptions {
90 pub graph: Option<Graph>,
92 pub name: Option<String>,
94 pub storage_prefix: Option<String>,
96 pub snapshot_key: Option<String>,
98 pub snapshot_store: Rc<dyn KvStorageTier<ReactiveCollectionSnapshotFrame>>,
100 pub change_log: Option<Rc<dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>>,
102 pub snapshot_on_attach: bool,
104 pub snapshot_every_changes: Option<usize>,
106}
107
108impl PersistReactiveCollectionOptions {
109 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
127pub struct ReactiveCollectionPersistence {
129 pub ready: Node<bool>,
131 pub status: Node<Value>,
133 pub error: Node<Value>,
135 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 pub fn flush(&self) -> StorageResult<()> {
145 (self.flush)()
146 }
147
148 pub fn snapshot(&self) -> StorageResult<()> {
150 (self.snapshot)()
151 }
152
153 pub fn dispose(&self) {
155 if let Some(dispose) = self.dispose.borrow_mut().take() {
156 dispose();
157 }
158 }
159
160 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 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 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
198pub struct OpenPersistentReactiveList<T> {
200 pub collection: ReactiveList<T>,
202 pub persistence: ReactiveCollectionPersistence,
204}
205
206pub struct OpenPersistentReactiveLog<T> {
208 pub collection: ReactiveLog<T>,
210 pub persistence: ReactiveCollectionPersistence,
212}
213
214pub struct OpenPersistentReactiveMap<K, V> {
216 pub collection: ReactiveMap<K, V>,
218 pub persistence: ReactiveCollectionPersistence,
220}
221
222pub struct OpenPersistentReactiveIndex<K, S, V> {
224 pub collection: ReactiveIndex<K, S, V>,
226 pub persistence: ReactiveCollectionPersistence,
228}
229
230pub struct OpenPersistentReactiveListOptions<T> {
232 pub initial: Vec<T>,
234 pub collection: ReactiveListOptions,
236 pub persistence: PersistReactiveCollectionOptions,
238}
239
240pub struct OpenPersistentReactiveLogOptions<T> {
242 pub initial: Vec<T>,
244 pub collection: ReactiveLogOptions,
246 pub persistence: PersistReactiveCollectionOptions,
248}
249
250pub struct OpenPersistentReactiveMapOptions<K, V> {
252 pub initial: Vec<(K, V)>,
254 pub collection: ReactiveMapOptions,
256 pub persistence: PersistReactiveCollectionOptions,
258}
259
260pub struct OpenPersistentReactiveIndexOptions<K, S, V> {
262 pub initial: Vec<IndexRow<K, S, V>>,
264 pub collection: ReactiveIndexOptions,
266 pub persistence: PersistReactiveCollectionOptions,
268}
269
270pub 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
289pub 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
308pub 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
328pub 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
428pub 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
456pub 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
484pub 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
516pub 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}