Skip to main content

graphrefly/
storage.rs

1//! Passive storage/read-through helpers for Rust product completeness (D123).
2//!
3//! This module is deliberately graph-agnostic: it creates no graph nodes, adds no
4//! graph storage methods, and does not participate in hydration/restore or wave
5//! protocol semantics.
6
7use std::cell::{Cell, RefCell};
8use std::collections::HashMap;
9use std::error::Error;
10use std::fmt;
11use std::fs::{self, OpenOptions};
12use std::io::{self, Write};
13use std::panic::{catch_unwind, AssertUnwindSafe};
14use std::path::{Path, PathBuf};
15use std::rc::Rc;
16use std::sync::atomic::{AtomicU64, Ordering};
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use serde::de::DeserializeOwned;
20use serde::{Deserialize, Serialize};
21use serde_json::Value;
22use sha2::{Digest, Sha256};
23
24use crate::data_structures::{IndexChange, IndexRow, ListChange, LogChange, MapChange};
25use crate::json::{strict_canonical_json_bytes, strict_json_decode, Codec, JsonCodecError};
26
27static NEXT_STORE_ID: AtomicU64 = AtomicU64::new(1);
28
29/// `StorageResult` type alias.
30pub type StorageResult<T> = Result<T, StorageError>;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33/// `StorageError` variants.
34pub enum StorageError {
35    /// `Unsupported` variant.
36    Unsupported {
37        /// `label` field for `Unsupported`.
38        label: String,
39        /// `capability` field for `Unsupported`.
40        capability: String,
41    },
42    /// `ContentAddressedMiss` variant.
43    ContentAddressedMiss {
44        /// `key` field for `ContentAddressedMiss`.
45        key: String,
46    },
47    /// `Backend` variant.
48    Backend(String),
49}
50
51impl StorageError {
52    /// Creates or computes `backend`.
53    pub fn backend(message: impl Into<String>) -> Self {
54        Self::Backend(message.into())
55    }
56
57    /// Creates or computes `unsupported`.
58    pub fn unsupported(label: impl Into<String>, capability: impl Into<String>) -> Self {
59        Self::Unsupported {
60            label: label.into(),
61            capability: capability.into(),
62        }
63    }
64}
65
66impl fmt::Display for StorageError {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::Unsupported { label, capability } => {
70                write!(f, "{label}: KV tier does not support {capability}")
71            }
72            Self::ContentAddressedMiss { key } => {
73                write!(
74                    f,
75                    "content-addressed lookup miss in read-strict mode: {key}"
76                )
77            }
78            Self::Backend(message) => f.write_str(message),
79        }
80    }
81}
82
83impl Error for StorageError {}
84
85/// `ByteStorageBackend` behavior contract.
86pub trait ByteStorageBackend {
87    /// Updates or reads `get`.
88    fn get(&self, key: &str) -> StorageResult<Option<Vec<u8>>>;
89    /// Updates or reads `put`.
90    fn put(&self, key: &str, value: &[u8]) -> StorageResult<()>;
91    /// Updates or reads `put_if_absent`.
92    fn put_if_absent(&self, _key: &str, _value: &[u8]) -> StorageResult<bool> {
93        Err(StorageError::unsupported("byteStorage", "put-if-absent"))
94    }
95    /// Updates or reads `delete`.
96    fn delete(&self, key: &str) -> StorageResult<()>;
97    /// Updates or reads `list`.
98    fn list(&self, prefix: &str) -> StorageResult<Vec<String>>;
99}
100
101const FILE_STEM_PREFIX: &str = "k-";
102const DEFAULT_FILE_EXTENSION: &str = ".bin";
103const STORAGE_NAMESPACE_PREFIX: &str = "storage-namespace";
104const STORAGE_NAMESPACE_PREFIX_WITH_COLON: &str = "storage-namespace:";
105
106fn storage_tuple_key(parts: &[&str]) -> String {
107    serde_json::to_string(parts).expect("storage tuple key encoding cannot fail")
108}
109
110fn parse_storage_tuple_key(value: &str) -> Option<Vec<String>> {
111    serde_json::from_str::<Vec<String>>(value).ok()
112}
113
114fn storage_physical_key(namespace: &str, logical_key: &str) -> String {
115    format!(
116        "{STORAGE_NAMESPACE_PREFIX}:{}",
117        storage_tuple_key(&[namespace, logical_key])
118    )
119}
120
121fn decode_storage_physical_key(
122    namespace: &str,
123    raw_key: &str,
124    malformed_message: &'static str,
125) -> StorageResult<Option<String>> {
126    let Some(tuple_key) = raw_key.strip_prefix(STORAGE_NAMESPACE_PREFIX_WITH_COLON) else {
127        return Ok(None);
128    };
129    let Some(tuple) = parse_storage_tuple_key(tuple_key) else {
130        return Err(StorageError::backend(malformed_message));
131    };
132    if tuple.first().map(String::as_str) != Some(namespace) {
133        return Ok(None);
134    }
135    if tuple.len() != 2 {
136        return Err(StorageError::backend(malformed_message));
137    }
138    Ok(Some(tuple[1].clone()))
139}
140
141fn content_addressed_storage_key(prefix: &str, hash_hex: &str) -> String {
142    format!("{prefix}:{}", storage_tuple_key(&[hash_hex]))
143}
144
145#[derive(Clone, Debug, Eq, PartialEq)]
146/// `FileBackendOptions` data container.
147pub struct FileBackendOptions {
148    /// `namespace` field for namespace.
149    pub namespace: String,
150    /// `extension` field for extension.
151    pub extension: String,
152}
153
154impl Default for FileBackendOptions {
155    fn default() -> Self {
156        Self {
157            namespace: String::new(),
158            extension: DEFAULT_FILE_EXTENSION.to_owned(),
159        }
160    }
161}
162
163impl FileBackendOptions {
164    /// Creates or computes `new`.
165    pub fn new() -> Self {
166        Self::default()
167    }
168
169    /// Updates or reads `with_namespace`.
170    pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
171        self.namespace = namespace.into();
172        self
173    }
174
175    /// Updates or reads `with_extension`.
176    pub fn with_extension(mut self, extension: impl Into<String>) -> Self {
177        self.extension = extension.into();
178        self
179    }
180}
181
182#[derive(Clone, Debug)]
183/// `FileBackend` data container.
184pub struct FileBackend {
185    dir: PathBuf,
186    namespace: String,
187    extension: String,
188}
189
190/// Creates or computes `file_backend`.
191pub fn file_backend(
192    dir: impl Into<PathBuf>,
193    opts: FileBackendOptions,
194) -> StorageResult<FileBackend> {
195    validate_namespace("fileBackend", &opts.namespace)?;
196    validate_extension(&opts.extension)?;
197    Ok(FileBackend {
198        dir: dir.into(),
199        namespace: opts.namespace,
200        extension: opts.extension,
201    })
202}
203
204impl FileBackend {
205    /// Updates or reads `dir`.
206    pub fn dir(&self) -> &Path {
207        &self.dir
208    }
209
210    fn storage_key(&self, key: &str) -> StorageResult<String> {
211        validate_logical_key("fileBackend", key)?;
212        Ok(storage_physical_key(&self.namespace, key))
213    }
214
215    fn path_for(&self, key: &str) -> StorageResult<PathBuf> {
216        let stem = key_to_file_stem(&self.storage_key(key)?);
217        Ok(self
218            .dir
219            .join(format!("{FILE_STEM_PREFIX}{stem}{}", self.extension)))
220    }
221
222    fn key_from_filename(&self, filename: &str) -> StorageResult<Option<String>> {
223        if filename.starts_with('.') || !filename.ends_with(&self.extension) {
224            return Ok(None);
225        }
226        let stem = &filename[..filename.len() - self.extension.len()];
227        let Some(raw_stem) = stem.strip_prefix(FILE_STEM_PREFIX) else {
228            return Ok(None);
229        };
230        let Some(key) = file_stem_to_key(raw_stem) else {
231            return Ok(None);
232        };
233        decode_storage_physical_key(&self.namespace, &key, "fileBackend: malformed stored key")
234    }
235}
236
237impl ByteStorageBackend for FileBackend {
238    fn get(&self, key: &str) -> StorageResult<Option<Vec<u8>>> {
239        match fs::read(self.path_for(key)?) {
240            Ok(bytes) => Ok(Some(bytes)),
241            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
242            Err(error) => Err(StorageError::backend(format!("fileBackend.get: {error}"))),
243        }
244    }
245
246    fn put(&self, key: &str, value: &[u8]) -> StorageResult<()> {
247        fs::create_dir_all(&self.dir)
248            .map_err(|err| StorageError::backend(format!("fileBackend.put: {err}")))?;
249        let file_path = self.path_for(key)?;
250        let file_name = file_path
251            .file_name()
252            .and_then(|name| name.to_str())
253            .ok_or_else(|| StorageError::backend("fileBackend.put: invalid file name"))?;
254        let tmp = write_temp_file(&self.dir, file_name, value, "fileBackend.put")?;
255        if let Err(error) = fs::rename(&tmp, &file_path) {
256            let _ = fs::remove_file(&tmp);
257            return Err(StorageError::backend(format!("fileBackend.put: {error}")));
258        }
259        Ok(())
260    }
261
262    fn put_if_absent(&self, key: &str, value: &[u8]) -> StorageResult<bool> {
263        fs::create_dir_all(&self.dir)
264            .map_err(|err| StorageError::backend(format!("fileBackend.put_if_absent: {err}")))?;
265        let file_path = self.path_for(key)?;
266        let file_name = file_path
267            .file_name()
268            .and_then(|name| name.to_str())
269            .ok_or_else(|| StorageError::backend("fileBackend.put_if_absent: invalid file name"))?;
270        let tmp = write_temp_file(&self.dir, file_name, value, "fileBackend.put_if_absent")?;
271        match fs::hard_link(&tmp, &file_path) {
272            Ok(()) => {
273                let _ = fs::remove_file(&tmp);
274                Ok(true)
275            }
276            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
277                let _ = fs::remove_file(&tmp);
278                Ok(false)
279            }
280            Err(error) => {
281                let _ = fs::remove_file(&tmp);
282                Err(StorageError::backend(format!(
283                    "fileBackend.put_if_absent: {error}"
284                )))
285            }
286        }
287    }
288
289    fn delete(&self, key: &str) -> StorageResult<()> {
290        match fs::remove_file(self.path_for(key)?) {
291            Ok(()) => Ok(()),
292            Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
293            Err(error) => Err(StorageError::backend(format!(
294                "fileBackend.delete: {error}"
295            ))),
296        }
297    }
298
299    fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
300        validate_list_prefix("fileBackend", prefix)?;
301        let entries = match fs::read_dir(&self.dir) {
302            Ok(entries) => entries,
303            Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
304            Err(error) => return Err(StorageError::backend(format!("fileBackend.list: {error}"))),
305        };
306        let mut keys = Vec::new();
307        for entry in entries {
308            let entry =
309                entry.map_err(|err| StorageError::backend(format!("fileBackend.list: {err}")))?;
310            let Some(filename) = entry.file_name().to_str().map(str::to_owned) else {
311                continue;
312            };
313            if let Some(key) = self.key_from_filename(&filename)? {
314                if key.starts_with(prefix) {
315                    keys.push(key);
316                }
317            }
318        }
319        keys.sort();
320        Ok(keys)
321    }
322}
323
324fn validate_namespace(label: &str, value: &str) -> StorageResult<()> {
325    let _ = (label, value);
326    Ok(())
327}
328
329fn validate_logical_key(label: &str, value: &str) -> StorageResult<()> {
330    let _ = (label, value);
331    Ok(())
332}
333
334fn validate_list_prefix(label: &str, value: &str) -> StorageResult<()> {
335    let _ = (label, value);
336    Ok(())
337}
338
339fn validate_extension(extension: &str) -> StorageResult<()> {
340    let valid = extension.len() >= 2
341        && extension.starts_with('.')
342        && !extension.contains("..")
343        && !extension.contains('/')
344        && !extension.contains('\\')
345        && !extension.contains('\0')
346        && extension
347            .bytes()
348            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'));
349    if valid {
350        Ok(())
351    } else {
352        Err(StorageError::backend(
353            "fileBackend: extension must be a simple suffix such as .bin",
354        ))
355    }
356}
357
358fn temp_file_path(dir: &Path, file_name: &str) -> PathBuf {
359    let nanos = SystemTime::now()
360        .duration_since(UNIX_EPOCH)
361        .map(|duration| duration.as_nanos())
362        .unwrap_or(0);
363    dir.join(format!(
364        ".{file_name}.{}.{}.{}.tmp",
365        std::process::id(),
366        nanos,
367        NEXT_STORE_ID.fetch_add(1, Ordering::Relaxed)
368    ))
369}
370
371fn write_temp_file(
372    dir: &Path,
373    file_name: &str,
374    value: &[u8],
375    label: &str,
376) -> StorageResult<PathBuf> {
377    for _ in 0..16 {
378        let tmp = temp_file_path(dir, file_name);
379        let mut file = match OpenOptions::new().write(true).create_new(true).open(&tmp) {
380            Ok(file) => file,
381            Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
382            Err(error) => return Err(StorageError::backend(format!("{label}: {error}"))),
383        };
384        if let Err(error) = file.write_all(value).and_then(|_| file.sync_all()) {
385            let _ = fs::remove_file(&tmp);
386            return Err(StorageError::backend(format!("{label}: {error}")));
387        }
388        return Ok(tmp);
389    }
390    Err(StorageError::backend(format!(
391        "{label}: could not allocate a unique temporary file"
392    )))
393}
394
395fn key_to_file_stem(key: &str) -> String {
396    let mut out = String::new();
397    for byte in key.bytes() {
398        if byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-') {
399            out.push(byte as char);
400        } else {
401            out.push('%');
402            out.push(hex_nibble(byte >> 4));
403            out.push(hex_nibble(byte & 0x0f));
404        }
405    }
406    out
407}
408
409fn hex_nibble(value: u8) -> char {
410    match value {
411        0..=9 => (b'0' + value) as char,
412        _ => (b'a' + (value - 10)) as char,
413    }
414}
415
416fn file_stem_to_key(stem: &str) -> Option<String> {
417    let bytes = stem.as_bytes();
418    let mut out = Vec::with_capacity(bytes.len());
419    let mut index = 0;
420    while index < bytes.len() {
421        if bytes[index] == b'%' && index + 2 < bytes.len() {
422            let hi = (bytes[index + 1] as char).to_digit(16)?;
423            let lo = (bytes[index + 2] as char).to_digit(16)?;
424            out.push(((hi << 4) | lo) as u8);
425            index += 3;
426        } else if bytes[index].is_ascii() {
427            out.push(bytes[index]);
428            index += 1;
429        } else {
430            return None;
431        }
432    }
433    let key = String::from_utf8(out).ok()?;
434    if key_to_file_stem(&key) == stem {
435        Some(key)
436    } else {
437        None
438    }
439}
440
441/// Opaque D108 per-key generation token for typed KV versioned reads.
442#[derive(Clone)]
443pub struct KvGeneration {
444    store_id: u64,
445    epoch: u64,
446    key: String,
447    version: u64,
448}
449
450impl KvGeneration {
451    fn new(store_id: u64, epoch: u64, key: &str, version: u64) -> Self {
452        Self {
453            store_id,
454            epoch,
455            key: key.to_owned(),
456            version,
457        }
458    }
459}
460
461impl fmt::Debug for KvGeneration {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        f.write_str("KvGeneration(<opaque>)")
464    }
465}
466
467#[derive(Clone, Debug)]
468/// `KvVersionedRead` variants.
469pub enum KvVersionedRead<T> {
470    /// `Hit` variant.
471    Hit {
472        /// `value` field for `Hit`.
473        value: T,
474        /// `generation` field for `Hit`.
475        generation: KvGeneration,
476    },
477    /// `Miss` variant.
478    Miss {
479        /// `generation` field for `Miss`.
480        generation: KvGeneration,
481    },
482}
483
484impl<T> KvVersionedRead<T> {
485    /// Updates or reads `generation`.
486    pub fn generation(&self) -> &KvGeneration {
487        match self {
488            Self::Hit { generation, .. } | Self::Miss { generation } => generation,
489        }
490    }
491}
492
493/// Typed string-key KV tier. Versioning is a narrow optional D108 capability.
494pub trait KvStorageTier<T: Clone> {
495    /// Updates or reads `get`.
496    fn get(&self, key: &str) -> StorageResult<Option<T>>;
497    /// Updates or reads `set`.
498    fn set(&self, key: &str, value: T) -> StorageResult<()>;
499    /// Updates or reads `put_if_absent`.
500    fn put_if_absent(&self, _key: &str, _value: T) -> StorageResult<bool> {
501        Err(StorageError::unsupported("kvStorage", "put-if-absent"))
502    }
503    /// Updates or reads `delete`.
504    fn delete(&self, key: &str) -> StorageResult<()>;
505    /// Updates or reads `list`.
506    fn list(&self, prefix: &str) -> StorageResult<Vec<String>>;
507
508    /// Updates or reads `supports_versioned`.
509    fn supports_versioned(&self) -> bool {
510        false
511    }
512
513    /// Updates or reads `get_versioned`.
514    fn get_versioned(&self, _key: &str) -> StorageResult<KvVersionedRead<T>> {
515        Err(StorageError::unsupported(
516            "kvStorage",
517            "versioned get/set-if-match",
518        ))
519    }
520
521    /// Updates or reads `set_if_match`.
522    fn set_if_match(
523        &self,
524        _key: &str,
525        _value: T,
526        _generation: &KvGeneration,
527    ) -> StorageResult<bool> {
528        Err(StorageError::unsupported(
529            "kvStorage",
530            "versioned get/set-if-match",
531        ))
532    }
533}
534
535#[derive(Debug)]
536struct MemoryEntry<T> {
537    value: T,
538    version: u64,
539}
540
541#[derive(Debug)]
542struct MemoryKvInner<T> {
543    store_id: u64,
544    epoch: Cell<u64>,
545    entries: RefCell<HashMap<String, MemoryEntry<T>>>,
546    tombstones: RefCell<HashMap<String, u64>>,
547    next_version: Cell<u64>,
548}
549
550/// In-memory typed KV tier with D108 opaque-generation support.
551#[derive(Clone, Debug)]
552pub struct MemoryKv<T: Clone> {
553    inner: Rc<MemoryKvInner<T>>,
554}
555
556/// Creates or computes `memory_kv`.
557pub fn memory_kv<T: Clone>() -> MemoryKv<T> {
558    MemoryKv {
559        inner: Rc::new(MemoryKvInner {
560            store_id: NEXT_STORE_ID.fetch_add(1, Ordering::Relaxed),
561            epoch: Cell::new(0),
562            entries: RefCell::new(HashMap::new()),
563            tombstones: RefCell::new(HashMap::new()),
564            next_version: Cell::new(1),
565        }),
566    }
567}
568
569/// Creates or computes `dict_kv`.
570pub fn dict_kv<T: Clone>(entries: impl IntoIterator<Item = (impl Into<String>, T)>) -> MemoryKv<T> {
571    let kv = memory_kv();
572    for (key, value) in entries {
573        kv.set(&key.into(), value)
574            .expect("memory_kv set is infallible");
575    }
576    kv
577}
578
579impl<T: Clone> MemoryKv<T> {
580    fn bump_version(&self) -> u64 {
581        let version = self.inner.next_version.get();
582        self.inner.next_version.set(version + 1);
583        version
584    }
585
586    fn current_version(&self, key: &str) -> u64 {
587        if let Some(entry) = self.inner.entries.borrow().get(key) {
588            entry.version
589        } else {
590            self.inner
591                .tombstones
592                .borrow()
593                .get(key)
594                .copied()
595                .unwrap_or(0)
596        }
597    }
598
599    fn generation_for(&self, key: &str) -> KvGeneration {
600        KvGeneration::new(
601            self.inner.store_id,
602            self.inner.epoch.get(),
603            key,
604            self.current_version(key),
605        )
606    }
607
608    /// Updates or reads `clear`.
609    pub fn clear(&self) {
610        self.inner.entries.borrow_mut().clear();
611        self.inner.tombstones.borrow_mut().clear();
612        self.inner.epoch.set(self.inner.epoch.get() + 1);
613        self.inner.next_version.set(1);
614    }
615}
616
617impl<T: Clone> KvStorageTier<T> for MemoryKv<T> {
618    fn get(&self, key: &str) -> StorageResult<Option<T>> {
619        Ok(self
620            .inner
621            .entries
622            .borrow()
623            .get(key)
624            .map(|entry| entry.value.clone()))
625    }
626
627    fn set(&self, key: &str, value: T) -> StorageResult<()> {
628        let version = self.bump_version();
629        self.inner
630            .entries
631            .borrow_mut()
632            .insert(key.to_owned(), MemoryEntry { value, version });
633        self.inner.tombstones.borrow_mut().remove(key);
634        Ok(())
635    }
636
637    fn put_if_absent(&self, key: &str, value: T) -> StorageResult<bool> {
638        if self.inner.entries.borrow().contains_key(key) {
639            return Ok(false);
640        }
641        self.set(key, value)?;
642        Ok(true)
643    }
644
645    fn delete(&self, key: &str) -> StorageResult<()> {
646        if self.inner.entries.borrow_mut().remove(key).is_some() {
647            let version = self.bump_version();
648            self.inner
649                .tombstones
650                .borrow_mut()
651                .insert(key.to_owned(), version);
652        }
653        Ok(())
654    }
655
656    fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
657        let mut keys = self
658            .inner
659            .entries
660            .borrow()
661            .keys()
662            .filter(|key| key.starts_with(prefix))
663            .cloned()
664            .collect::<Vec<_>>();
665        keys.sort();
666        Ok(keys)
667    }
668
669    fn supports_versioned(&self) -> bool {
670        true
671    }
672
673    fn get_versioned(&self, key: &str) -> StorageResult<KvVersionedRead<T>> {
674        if let Some(entry) = self.inner.entries.borrow().get(key) {
675            return Ok(KvVersionedRead::Hit {
676                value: entry.value.clone(),
677                generation: KvGeneration::new(
678                    self.inner.store_id,
679                    self.inner.epoch.get(),
680                    key,
681                    entry.version,
682                ),
683            });
684        }
685        Ok(KvVersionedRead::Miss {
686            generation: self.generation_for(key),
687        })
688    }
689
690    fn set_if_match(&self, key: &str, value: T, generation: &KvGeneration) -> StorageResult<bool> {
691        if generation.store_id != self.inner.store_id
692            || generation.epoch != self.inner.epoch.get()
693            || generation.key != key
694            || generation.version != self.current_version(key)
695        {
696            return Ok(false);
697        }
698        self.set(key, value)?;
699        Ok(true)
700    }
701}
702
703#[derive(Clone, Debug)]
704/// `CodecKvStorage` data container.
705pub struct CodecKvStorage<B, C, T> {
706    backend: B,
707    codec: C,
708    marker: std::marker::PhantomData<T>,
709}
710
711/// Creates or computes `codec_kv_storage`.
712pub fn codec_kv_storage<B, C, T>(backend: B, codec: C) -> CodecKvStorage<B, C, T>
713where
714    B: ByteStorageBackend + Clone,
715    C: Codec<T> + Clone,
716    T: Clone,
717{
718    CodecKvStorage {
719        backend,
720        codec,
721        marker: std::marker::PhantomData,
722    }
723}
724
725impl<B, C, T> KvStorageTier<T> for CodecKvStorage<B, C, T>
726where
727    B: ByteStorageBackend + Clone,
728    C: Codec<T> + Clone,
729    T: Clone,
730{
731    fn get(&self, key: &str) -> StorageResult<Option<T>> {
732        self.backend
733            .get(key)?
734            .map(|bytes| self.codec.decode(&bytes).map_err(storage_json_error))
735            .transpose()
736    }
737
738    fn set(&self, key: &str, value: T) -> StorageResult<()> {
739        let bytes = self.codec.encode(&value).map_err(storage_json_error)?;
740        self.backend.put(key, &bytes)
741    }
742
743    fn put_if_absent(&self, key: &str, value: T) -> StorageResult<bool> {
744        let bytes = self.codec.encode(&value).map_err(storage_json_error)?;
745        self.backend.put_if_absent(key, &bytes)
746    }
747
748    fn delete(&self, key: &str) -> StorageResult<()> {
749        self.backend.delete(key)
750    }
751
752    fn list(&self, prefix: &str) -> StorageResult<Vec<String>> {
753        self.backend.list(prefix)
754    }
755}
756
757/// `FileKv` type alias.
758pub type FileKv<T, C> = CodecKvStorage<FileBackend, C, T>;
759
760/// Creates or computes `file_kv`.
761pub fn file_kv<T, C>(
762    dir: impl Into<PathBuf>,
763    opts: FileBackendOptions,
764    codec: C,
765) -> StorageResult<FileKv<T, C>>
766where
767    C: Codec<T> + Clone,
768    T: Clone,
769{
770    Ok(codec_kv_storage(file_backend(dir, opts)?, codec))
771}
772
773#[derive(Clone, Debug, Eq, PartialEq)]
774/// `FileAppendLogOptions` data container.
775pub struct FileAppendLogOptions {
776    /// `backend` field for backend.
777    pub backend: FileBackendOptions,
778    /// `prefix` field for prefix.
779    pub prefix: String,
780}
781
782impl Default for FileAppendLogOptions {
783    fn default() -> Self {
784        Self {
785            backend: FileBackendOptions::default(),
786            prefix: "event-log".to_owned(),
787        }
788    }
789}
790
791impl FileAppendLogOptions {
792    /// Creates or computes `new`.
793    pub fn new() -> Self {
794        Self::default()
795    }
796
797    /// Updates or reads `with_backend`.
798    pub fn with_backend(mut self, backend: FileBackendOptions) -> Self {
799        self.backend = backend;
800        self
801    }
802
803    /// Updates or reads `with_prefix`.
804    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
805        self.prefix = prefix.into();
806        self
807    }
808}
809
810/// Creates or computes `file_append_log`.
811pub fn file_append_log<T, C>(
812    dir: impl Into<PathBuf>,
813    opts: FileAppendLogOptions,
814    codec: C,
815) -> StorageResult<AppendLogStorage<T>>
816where
817    C: Codec<T> + Clone + 'static,
818    T: Clone + 'static,
819{
820    let kv = file_kv(dir, opts.backend, codec)?;
821    Ok(append_log_storage(Rc::new(kv), opts.prefix))
822}
823
824#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
825/// `ContentAddressedMode` variants.
826pub enum ContentAddressedMode {
827    /// `Read` variant.
828    Read,
829    /// `Write` variant.
830    Write,
831    #[default]
832    /// `ReadWrite` variant.
833    ReadWrite,
834    /// `ReadStrict` variant.
835    ReadStrict,
836}
837
838/// `ContentAddressedKeyContext` type alias.
839pub type ContentAddressedKeyContext<Ctx> = dyn Fn(&Ctx) -> Result<Value, JsonCodecError>;
840
841/// `ContentAddressedKvOptions` data container.
842pub struct ContentAddressedKvOptions<Ctx, V: Clone> {
843    /// `kv` field for kv.
844    pub kv: Rc<dyn KvStorageTier<V>>,
845    /// `key_context` field for key context.
846    pub key_context: Rc<ContentAddressedKeyContext<Ctx>>,
847    /// `key_prefix` field for key prefix.
848    pub key_prefix: Option<String>,
849    /// `mode` field for mode.
850    pub mode: ContentAddressedMode,
851}
852
853impl<V: Clone> ContentAddressedKvOptions<Value, V> {
854    /// Creates or computes `new`.
855    pub fn new(kv: Rc<dyn KvStorageTier<V>>) -> Self {
856        Self {
857            kv,
858            key_context: Rc::new(|ctx| Ok(ctx.clone())),
859            key_prefix: None,
860            mode: ContentAddressedMode::ReadWrite,
861        }
862    }
863}
864
865impl<Ctx, V: Clone> ContentAddressedKvOptions<Ctx, V> {
866    /// Creates or computes `from_key_context`.
867    pub fn from_key_context(
868        kv: Rc<dyn KvStorageTier<V>>,
869        f: impl Fn(&Ctx) -> Result<Value, JsonCodecError> + 'static,
870    ) -> Self {
871        Self {
872            kv,
873            key_context: Rc::new(f),
874            key_prefix: None,
875            mode: ContentAddressedMode::ReadWrite,
876        }
877    }
878
879    /// Updates or reads `with_key_context`.
880    pub fn with_key_context(
881        mut self,
882        f: impl Fn(&Ctx) -> Result<Value, JsonCodecError> + 'static,
883    ) -> Self {
884        self.key_context = Rc::new(f);
885        self
886    }
887
888    /// Updates or reads `with_key_prefix`.
889    pub fn with_key_prefix(mut self, prefix: impl Into<String>) -> Self {
890        self.key_prefix = Some(prefix.into());
891        self
892    }
893
894    /// Updates or reads `with_mode`.
895    pub fn with_mode(mut self, mode: ContentAddressedMode) -> Self {
896        self.mode = mode;
897        self
898    }
899}
900
901#[derive(Clone)]
902/// `ContentAddressedKv` data container.
903pub struct ContentAddressedKv<Ctx, V: Clone> {
904    kv: Rc<dyn KvStorageTier<V>>,
905    key_context: Rc<ContentAddressedKeyContext<Ctx>>,
906    key_prefix: Option<String>,
907    mode: ContentAddressedMode,
908}
909
910impl<Ctx, V: Clone> fmt::Debug for ContentAddressedKv<Ctx, V> {
911    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
912        f.debug_struct("ContentAddressedKv")
913            .field("key_prefix", &self.key_prefix)
914            .field("mode", &self.mode)
915            .finish_non_exhaustive()
916    }
917}
918
919/// `ContentAddressedStorage` type alias.
920pub type ContentAddressedStorage<Ctx, V> = ContentAddressedKv<Ctx, V>;
921/// `ContentAddressedStorageOptions` type alias.
922pub type ContentAddressedStorageOptions<Ctx, V> = ContentAddressedKvOptions<Ctx, V>;
923
924/// Creates or computes `content_addressed_kv`.
925pub fn content_addressed_kv<Ctx, V: Clone>(
926    opts: ContentAddressedKvOptions<Ctx, V>,
927) -> ContentAddressedKv<Ctx, V> {
928    ContentAddressedKv {
929        kv: opts.kv,
930        key_context: opts.key_context,
931        key_prefix: opts.key_prefix,
932        mode: opts.mode,
933    }
934}
935
936/// Creates or computes `content_addressed_storage`.
937pub fn content_addressed_storage<Ctx, V: Clone>(
938    opts: ContentAddressedStorageOptions<Ctx, V>,
939) -> ContentAddressedStorage<Ctx, V> {
940    content_addressed_kv(opts)
941}
942
943impl<Ctx, V: Clone> ContentAddressedKv<Ctx, V> {
944    /// Updates or reads `key_for`.
945    pub fn key_for(&self, ctx: &Ctx) -> StorageResult<String> {
946        let context = (self.key_context)(ctx).map_err(storage_json_error)?;
947        let bytes = strict_canonical_json_bytes(&context).map_err(storage_json_error)?;
948        let hex = sha256_hex(&bytes);
949        Ok(match &self.key_prefix {
950            Some(prefix) => content_addressed_storage_key(prefix, &hex),
951            None => hex,
952        })
953    }
954
955    /// Updates or reads `lookup`.
956    pub fn lookup(&self, ctx: &Ctx) -> StorageResult<Option<V>> {
957        if self.mode == ContentAddressedMode::Write {
958            return Ok(None);
959        }
960        let key = self.key_for(ctx)?;
961        let value = self.kv.get(&key)?;
962        if value.is_none() && self.mode == ContentAddressedMode::ReadStrict {
963            return Err(StorageError::ContentAddressedMiss { key });
964        }
965        Ok(value)
966    }
967
968    /// Updates or reads `store`.
969    pub fn store(&self, ctx: &Ctx, value: V) -> StorageResult<()> {
970        if self.mode == ContentAddressedMode::Read {
971            return Ok(());
972        }
973        let key = self.key_for(ctx)?;
974        self.kv.set(&key, value)
975    }
976
977    /// Updates or reads `forget`.
978    pub fn forget(&self, ctx: &Ctx) -> StorageResult<()> {
979        if matches!(
980            self.mode,
981            ContentAddressedMode::Read | ContentAddressedMode::Write
982        ) {
983            return Ok(());
984        }
985        let key = self.key_for(ctx)?;
986        self.kv.delete(&key)
987    }
988}
989
990fn storage_json_error(error: JsonCodecError) -> StorageError {
991    StorageError::backend(error.to_string())
992}
993
994fn sha256_hex(bytes: &[u8]) -> String {
995    const HEX: &[u8; 16] = b"0123456789abcdef";
996    let digest = Sha256::digest(bytes);
997    let mut out = String::with_capacity(digest.len() * 2);
998    for byte in digest {
999        out.push(HEX[(byte >> 4) as usize] as char);
1000        out.push(HEX[(byte & 0x0f) as usize] as char);
1001    }
1002    out
1003}
1004
1005#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1006#[serde(rename_all = "lowercase")]
1007/// `ChangeLifecycle` variants.
1008pub enum ChangeLifecycle {
1009    /// `Spec` variant.
1010    Spec,
1011    /// `Data` variant.
1012    Data,
1013    /// `Ownership` variant.
1014    Ownership,
1015}
1016
1017/// `constant` constant.
1018pub const WAL_KEY_SEGMENT: &str = "wal";
1019/// `constant` constant.
1020pub const WAL_FRAME_SEQ_PAD: usize = 20;
1021/// `constant` constant.
1022pub const WAL_FORMAT_VERSION: u64 = 1;
1023
1024/// `WalFrameTimestampNs` type alias.
1025pub type WalFrameTimestampNs = String;
1026
1027#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1028/// `WalFrameBody` data container.
1029pub struct WalFrameBody<T> {
1030    /// `t` field for t.
1031    pub t: String,
1032    /// `lifecycle` field for lifecycle.
1033    pub lifecycle: ChangeLifecycle,
1034    /// `path` field for path.
1035    pub path: String,
1036    /// `change` field for change.
1037    pub change: T,
1038    /// `frame_seq` field for frame seq.
1039    pub frame_seq: u64,
1040    /// `frame_t_ns` field for frame t ns.
1041    pub frame_t_ns: WalFrameTimestampNs,
1042    /// `format_version` field for format version.
1043    pub format_version: u64,
1044}
1045
1046#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1047/// `WalFrame` data container.
1048pub struct WalFrame<T> {
1049    /// `t` field for t.
1050    pub t: String,
1051    /// `lifecycle` field for lifecycle.
1052    pub lifecycle: ChangeLifecycle,
1053    /// `path` field for path.
1054    pub path: String,
1055    /// `change` field for change.
1056    pub change: T,
1057    /// `frame_seq` field for frame seq.
1058    pub frame_seq: u64,
1059    /// `frame_t_ns` field for frame t ns.
1060    pub frame_t_ns: WalFrameTimestampNs,
1061    /// `format_version` field for format version.
1062    pub format_version: u64,
1063    /// `checksum` field for checksum.
1064    pub checksum: String,
1065}
1066
1067#[derive(Clone, Debug, PartialEq)]
1068/// `WalFrameOptions` data container.
1069pub struct WalFrameOptions<T> {
1070    /// `path` field for path.
1071    pub path: String,
1072    /// `change` field for change.
1073    pub change: T,
1074    /// `frame_seq` field for frame seq.
1075    pub frame_seq: u64,
1076    /// `lifecycle` field for lifecycle.
1077    pub lifecycle: ChangeLifecycle,
1078    /// `frame_t_ns` field for frame t ns.
1079    pub frame_t_ns: Option<String>,
1080}
1081
1082impl<T> WalFrameOptions<T> {
1083    /// Creates or computes `new`.
1084    pub fn new(path: impl Into<String>, change: T, frame_seq: u64) -> Self {
1085        Self {
1086            path: path.into(),
1087            change,
1088            frame_seq,
1089            lifecycle: ChangeLifecycle::Data,
1090            frame_t_ns: None,
1091        }
1092    }
1093
1094    /// Updates or reads `with_lifecycle`.
1095    pub fn with_lifecycle(mut self, lifecycle: ChangeLifecycle) -> Self {
1096        self.lifecycle = lifecycle;
1097        self
1098    }
1099
1100    /// Updates or reads `with_frame_t_ns`.
1101    pub fn with_frame_t_ns(mut self, frame_t_ns: impl Into<String>) -> Self {
1102        self.frame_t_ns = Some(frame_t_ns.into());
1103        self
1104    }
1105}
1106
1107/// Creates or computes `wal_frame_prefix`.
1108pub fn wal_frame_prefix(namespace: &str) -> String {
1109    if namespace.is_empty() {
1110        WAL_KEY_SEGMENT.to_owned()
1111    } else {
1112        format!("{namespace}/{WAL_KEY_SEGMENT}")
1113    }
1114}
1115
1116/// Creates or computes `wal_frame_key`.
1117pub fn wal_frame_key(prefix: &str, frame_seq: u64) -> String {
1118    format!("{prefix}/{frame_seq:0>width$}", width = WAL_FRAME_SEQ_PAD)
1119}
1120
1121/// Creates or computes `wal_frame_checksum`.
1122pub fn wal_frame_checksum<T: Serialize>(body: &WalFrameBody<T>) -> StorageResult<String> {
1123    assert_wal_frame_body(body).map_err(storage_json_error)?;
1124    let value = serde_json::to_value(body).map_err(|err| StorageError::backend(err.to_string()))?;
1125    assert_wal_frame_body_value(&value, "walFrameCodec").map_err(storage_json_error)?;
1126    let bytes = strict_canonical_json_bytes(&value).map_err(storage_json_error)?;
1127    Ok(sha256_hex(&bytes))
1128}
1129
1130/// Creates or computes `wal_frame`.
1131pub fn wal_frame<T: Serialize>(opts: WalFrameOptions<T>) -> StorageResult<WalFrame<T>> {
1132    let body = WalFrameBody {
1133        t: "c".to_owned(),
1134        lifecycle: opts.lifecycle,
1135        path: opts.path,
1136        change: opts.change,
1137        frame_seq: opts.frame_seq,
1138        frame_t_ns: match opts.frame_t_ns {
1139            Some(value) => crate::json::assert_non_negative_decimal_integer_string(
1140                value,
1141                "walFrameCodec: frame_t_ns",
1142            )
1143            .map_err(storage_json_error)?,
1144            None => now_ns(),
1145        },
1146        format_version: WAL_FORMAT_VERSION,
1147    };
1148    let checksum = wal_frame_checksum(&body)?;
1149    Ok(WalFrame {
1150        t: body.t,
1151        lifecycle: body.lifecycle,
1152        path: body.path,
1153        change: body.change,
1154        frame_seq: body.frame_seq,
1155        frame_t_ns: body.frame_t_ns,
1156        format_version: body.format_version,
1157        checksum,
1158    })
1159}
1160
1161/// Creates or computes `assert_wal_frame`.
1162pub fn assert_wal_frame<T: Serialize>(frame: &WalFrame<T>) -> crate::json::JsonCodecResult<()> {
1163    assert_wal_frame_body(&WalFrameBody {
1164        t: frame.t.clone(),
1165        lifecycle: frame.lifecycle.clone(),
1166        path: frame.path.clone(),
1167        change: &frame.change,
1168        frame_seq: frame.frame_seq,
1169        frame_t_ns: frame.frame_t_ns.clone(),
1170        format_version: frame.format_version,
1171    })?;
1172    if !is_sha256_hex(&frame.checksum) {
1173        return Err(JsonCodecError::validation(
1174            "walFrameCodec: checksum must be a lowercase sha256 hex string",
1175        ));
1176    }
1177    Ok(())
1178}
1179
1180/// Creates or computes `verify_wal_frame_checksum`.
1181pub fn verify_wal_frame_checksum<T: Serialize>(frame: &WalFrame<T>) -> StorageResult<bool> {
1182    assert_wal_frame(frame).map_err(storage_json_error)?;
1183    let body = WalFrameBody {
1184        t: frame.t.clone(),
1185        lifecycle: frame.lifecycle.clone(),
1186        path: frame.path.clone(),
1187        change: &frame.change,
1188        frame_seq: frame.frame_seq,
1189        frame_t_ns: frame.frame_t_ns.clone(),
1190        format_version: frame.format_version,
1191    };
1192    Ok(wal_frame_checksum(&body)? == frame.checksum)
1193}
1194
1195#[derive(Clone, Debug, Default)]
1196/// `WalFrameCodec` data container.
1197pub struct WalFrameCodec<T> {
1198    marker: std::marker::PhantomData<T>,
1199}
1200
1201/// Creates or computes `wal_frame_codec`.
1202pub fn wal_frame_codec<T>() -> WalFrameCodec<T> {
1203    WalFrameCodec {
1204        marker: std::marker::PhantomData,
1205    }
1206}
1207
1208impl<T> Codec<WalFrame<T>> for WalFrameCodec<T>
1209where
1210    T: Serialize + DeserializeOwned,
1211{
1212    fn encode(&self, value: &WalFrame<T>) -> crate::json::JsonCodecResult<Vec<u8>> {
1213        assert_wal_frame(value)?;
1214        let value =
1215            serde_json::to_value(value).map_err(|err| JsonCodecError::encode(err.to_string()))?;
1216        assert_wal_frame_value(&value)?;
1217        strict_canonical_json_bytes(&value)
1218    }
1219
1220    fn decode(&self, bytes: &[u8]) -> crate::json::JsonCodecResult<WalFrame<T>> {
1221        let value = strict_json_decode(bytes)?;
1222        assert_wal_frame_value(&value)?;
1223        serde_json::from_value(value).map_err(|err| JsonCodecError::decode(err.to_string()))
1224    }
1225}
1226
1227fn assert_wal_frame_body<T: Serialize>(body: &WalFrameBody<T>) -> crate::json::JsonCodecResult<()> {
1228    if body.t != "c" {
1229        return Err(JsonCodecError::validation("walFrameCodec: t must be c"));
1230    }
1231    if body.path.is_empty() {
1232        return Err(JsonCodecError::validation(
1233            "walFrameCodec: path must be a non-empty string",
1234        ));
1235    }
1236    crate::json::assert_non_negative_decimal_integer_string(
1237        body.frame_t_ns.clone(),
1238        "walFrameCodec: frame_t_ns",
1239    )?;
1240    if body.format_version != WAL_FORMAT_VERSION {
1241        return Err(JsonCodecError::validation(format!(
1242            "walFrameCodec: format_version must be {WAL_FORMAT_VERSION}"
1243        )));
1244    }
1245    let value =
1246        serde_json::to_value(body).map_err(|err| JsonCodecError::encode(err.to_string()))?;
1247    assert_wal_frame_body_value(&value, "walFrameCodec")
1248}
1249
1250fn assert_wal_frame_value(value: &Value) -> crate::json::JsonCodecResult<()> {
1251    assert_wal_frame_body_value(value, "walFrameCodec")?;
1252    let Some(record) = value.as_object() else {
1253        return Err(JsonCodecError::validation(
1254            "walFrameCodec: frame must be an object",
1255        ));
1256    };
1257    if !record.contains_key("checksum") {
1258        return Err(JsonCodecError::validation(
1259            "walFrameCodec: checksum is required",
1260        ));
1261    }
1262    let Some(checksum) = record.get("checksum").and_then(Value::as_str) else {
1263        return Err(JsonCodecError::validation(
1264            "walFrameCodec: checksum must be a lowercase sha256 hex string",
1265        ));
1266    };
1267    if !is_sha256_hex(checksum) {
1268        return Err(JsonCodecError::validation(
1269            "walFrameCodec: checksum must be a lowercase sha256 hex string",
1270        ));
1271    }
1272    Ok(())
1273}
1274
1275fn assert_wal_frame_body_value(value: &Value, label: &str) -> crate::json::JsonCodecResult<()> {
1276    let Some(record) = value.as_object() else {
1277        return Err(JsonCodecError::validation(format!(
1278            "{label}: frame must be an object"
1279        )));
1280    };
1281    for key in record.keys() {
1282        match key.as_str() {
1283            "t" | "lifecycle" | "path" | "change" | "frame_seq" | "frame_t_ns"
1284            | "format_version" | "checksum" => {}
1285            _ => {
1286                return Err(JsonCodecError::validation(format!(
1287                    "walFrameCodec: unknown field {key}"
1288                )))
1289            }
1290        }
1291    }
1292    match record.get("t").and_then(Value::as_str) {
1293        Some("c") => {}
1294        _ => return Err(JsonCodecError::validation("walFrameCodec: t must be c")),
1295    }
1296    match record.get("lifecycle").and_then(Value::as_str) {
1297        Some("spec" | "data" | "ownership") => {}
1298        _ => {
1299            return Err(JsonCodecError::validation(
1300                "walFrameCodec: lifecycle must be spec, data, or ownership",
1301            ))
1302        }
1303    }
1304    match record.get("path").and_then(Value::as_str) {
1305        Some(path) if !path.is_empty() => {}
1306        _ => {
1307            return Err(JsonCodecError::validation(
1308                "walFrameCodec: path must be a non-empty string",
1309            ))
1310        }
1311    }
1312    if !record.contains_key("change") {
1313        return Err(JsonCodecError::validation(
1314            "walFrameCodec: change payload is required",
1315        ));
1316    }
1317    if record.get("frame_seq").and_then(Value::as_u64).is_none() {
1318        return Err(JsonCodecError::validation(
1319            "walFrameCodec: frame_seq must be a non-negative integer",
1320        ));
1321    }
1322    let Some(frame_t_ns) = record.get("frame_t_ns").and_then(Value::as_str) else {
1323        return Err(JsonCodecError::validation(
1324            "walFrameCodec: frame_t_ns must be a canonical non-negative decimal integer string",
1325        ));
1326    };
1327    crate::json::assert_non_negative_decimal_integer_string(
1328        frame_t_ns,
1329        "walFrameCodec: frame_t_ns",
1330    )?;
1331    if record.get("format_version").and_then(Value::as_u64) != Some(WAL_FORMAT_VERSION) {
1332        return Err(JsonCodecError::validation(format!(
1333            "walFrameCodec: format_version must be {WAL_FORMAT_VERSION}"
1334        )));
1335    }
1336    Ok(())
1337}
1338
1339fn is_sha256_hex(value: &str) -> bool {
1340    value.len() == 64
1341        && value
1342            .bytes()
1343            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1344}
1345
1346#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1347/// `ChangeEnvelope` data container.
1348pub struct ChangeEnvelope<T> {
1349    /// `lifecycle` field for lifecycle.
1350    pub lifecycle: ChangeLifecycle,
1351    /// `structure` field for structure.
1352    pub structure: String,
1353    /// `version` field for version.
1354    pub version: Value,
1355    /// `t_ns` field for t ns.
1356    pub t_ns: String,
1357    #[serde(skip_serializing_if = "Option::is_none")]
1358    /// `seq` field for seq.
1359    pub seq: Option<u64>,
1360    /// `change` field for change.
1361    pub change: T,
1362}
1363
1364#[derive(Clone, Debug)]
1365/// `ChangeEnvelopeOptions` data container.
1366pub struct ChangeEnvelopeOptions {
1367    /// `lifecycle` field for lifecycle.
1368    pub lifecycle: ChangeLifecycle,
1369    /// `structure` field for structure.
1370    pub structure: String,
1371    /// `version` field for version.
1372    pub version: Value,
1373    /// `t_ns` field for t ns.
1374    pub t_ns: Option<String>,
1375    /// `seq` field for seq.
1376    pub seq: Option<u64>,
1377}
1378
1379impl ChangeEnvelopeOptions {
1380    /// Creates or computes `new`.
1381    pub fn new(structure: impl Into<String>) -> Self {
1382        Self {
1383            lifecycle: ChangeLifecycle::Data,
1384            structure: structure.into(),
1385            version: Value::from(1),
1386            t_ns: None,
1387            seq: None,
1388        }
1389    }
1390
1391    /// Updates or reads `with_lifecycle`.
1392    pub fn with_lifecycle(mut self, lifecycle: ChangeLifecycle) -> Self {
1393        self.lifecycle = lifecycle;
1394        self
1395    }
1396
1397    /// Updates or reads `with_version`.
1398    pub fn with_version(mut self, version: impl Into<Value>) -> Self {
1399        self.version = version.into();
1400        self
1401    }
1402
1403    /// Updates or reads `with_t_ns`.
1404    pub fn with_t_ns(mut self, t_ns: impl Into<String>) -> Self {
1405        self.t_ns = Some(t_ns.into());
1406        self
1407    }
1408
1409    /// Updates or reads `with_seq`.
1410    pub fn with_seq(mut self, seq: u64) -> Self {
1411        self.seq = Some(seq);
1412        self
1413    }
1414}
1415
1416/// Creates or computes `now_ns`.
1417pub fn now_ns() -> String {
1418    SystemTime::now()
1419        .duration_since(UNIX_EPOCH)
1420        .map(|duration| duration.as_nanos().to_string())
1421        .unwrap_or_else(|_| "0".to_owned())
1422}
1423
1424/// Creates or computes `envelope_change`.
1425pub fn envelope_change<T>(
1426    change: T,
1427    opts: ChangeEnvelopeOptions,
1428) -> StorageResult<ChangeEnvelope<T>> {
1429    if opts.structure.is_empty() {
1430        return Err(StorageError::backend(
1431            "changeEnvelopeCodec: structure must be a non-empty string",
1432        ));
1433    }
1434    let t_ns = match opts.t_ns {
1435        Some(value) => crate::json::assert_non_negative_decimal_integer_string(
1436            value,
1437            "changeEnvelopeCodec: t_ns",
1438        )
1439        .map_err(storage_json_error)?,
1440        None => now_ns(),
1441    };
1442    let envelope = ChangeEnvelope {
1443        lifecycle: opts.lifecycle,
1444        structure: opts.structure,
1445        version: opts.version,
1446        t_ns,
1447        seq: opts.seq,
1448        change,
1449    };
1450    assert_change_envelope(&envelope).map_err(storage_json_error)?;
1451    Ok(envelope)
1452}
1453
1454#[derive(Clone, Debug, Default)]
1455/// `ChangeEnvelopeCodec` data container.
1456pub struct ChangeEnvelopeCodec<T> {
1457    marker: std::marker::PhantomData<T>,
1458}
1459
1460/// Creates or computes `change_envelope_codec`.
1461pub fn change_envelope_codec<T>() -> ChangeEnvelopeCodec<T> {
1462    ChangeEnvelopeCodec {
1463        marker: std::marker::PhantomData,
1464    }
1465}
1466
1467impl<T> Codec<ChangeEnvelope<T>> for ChangeEnvelopeCodec<T>
1468where
1469    T: Serialize + DeserializeOwned,
1470{
1471    fn encode(&self, value: &ChangeEnvelope<T>) -> crate::json::JsonCodecResult<Vec<u8>> {
1472        assert_change_envelope(value)?;
1473        let value =
1474            serde_json::to_value(value).map_err(|err| JsonCodecError::encode(err.to_string()))?;
1475        strict_canonical_json_bytes(&value)
1476    }
1477
1478    fn decode(&self, bytes: &[u8]) -> crate::json::JsonCodecResult<ChangeEnvelope<T>> {
1479        let value = strict_json_decode(bytes)?;
1480        assert_change_envelope_value(&value, "changeEnvelopeCodec")?;
1481        serde_json::from_value(value).map_err(|err| JsonCodecError::decode(err.to_string()))
1482    }
1483}
1484
1485/// Creates or computes `assert_change_envelope`.
1486pub fn assert_change_envelope<T>(value: &ChangeEnvelope<T>) -> crate::json::JsonCodecResult<()> {
1487    if value.structure.is_empty() {
1488        return Err(JsonCodecError::validation(
1489            "changeEnvelopeCodec: structure must be a non-empty string",
1490        ));
1491    }
1492    crate::json::assert_non_negative_decimal_integer_string(
1493        value.t_ns.clone(),
1494        "changeEnvelopeCodec: t_ns",
1495    )?;
1496    match &value.version {
1497        Value::Number(_) | Value::String(_) => Ok(()),
1498        _ => Err(JsonCodecError::validation(
1499            "changeEnvelopeCodec: version must be a finite number or string",
1500        )),
1501    }
1502}
1503
1504fn assert_change_envelope_value(value: &Value, label: &str) -> crate::json::JsonCodecResult<()> {
1505    let Some(record) = value.as_object() else {
1506        return Err(JsonCodecError::validation(format!(
1507            "{label}: frame must be an object"
1508        )));
1509    };
1510    match record.get("lifecycle").and_then(Value::as_str) {
1511        Some("spec" | "data" | "ownership") => {}
1512        _ => {
1513            return Err(JsonCodecError::validation(
1514                "changeEnvelopeCodec: lifecycle must be spec, data, or ownership",
1515            ))
1516        }
1517    }
1518    match record.get("structure").and_then(Value::as_str) {
1519        Some(structure) if !structure.is_empty() => {}
1520        _ => {
1521            return Err(JsonCodecError::validation(
1522                "changeEnvelopeCodec: structure must be a non-empty string",
1523            ))
1524        }
1525    }
1526    match record.get("version") {
1527        Some(Value::Number(_) | Value::String(_)) => {}
1528        _ => {
1529            return Err(JsonCodecError::validation(
1530                "changeEnvelopeCodec: version must be a finite number or string",
1531            ))
1532        }
1533    }
1534    let Some(t_ns) = record.get("t_ns").and_then(Value::as_str) else {
1535        return Err(JsonCodecError::validation(
1536            "changeEnvelopeCodec: t_ns must be a canonical non-negative decimal integer string",
1537        ));
1538    };
1539    crate::json::assert_non_negative_decimal_integer_string(t_ns, "changeEnvelopeCodec: t_ns")?;
1540    if record.get("seq").is_some_and(|seq| seq.as_u64().is_none()) {
1541        return Err(JsonCodecError::validation(
1542            "changeEnvelopeCodec: seq must be a non-negative integer when present",
1543        ));
1544    }
1545    if !record.contains_key("change") {
1546        return Err(JsonCodecError::validation(
1547            "changeEnvelopeCodec: change payload is required",
1548        ));
1549    }
1550    Ok(())
1551}
1552
1553#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1554/// `ObserveEventFrame` data container.
1555pub struct ObserveEventFrame<T> {
1556    /// `lifecycle` field for lifecycle.
1557    pub lifecycle: ChangeLifecycle,
1558    /// `structure` field for structure.
1559    pub structure: String,
1560    /// `version` field for version.
1561    pub version: Value,
1562    /// `t_ns` field for t ns.
1563    pub t_ns: String,
1564    #[serde(skip_serializing_if = "Option::is_none")]
1565    /// `seq` field for seq.
1566    pub seq: Option<u64>,
1567    /// `change` field for change.
1568    pub change: T,
1569    #[serde(rename = "observeSeq")]
1570    /// `observe_seq` field for observe seq.
1571    pub observe_seq: u64,
1572    /// `path` field for path.
1573    pub path: String,
1574    #[serde(skip_serializing_if = "Option::is_none")]
1575    /// `stream` field for stream.
1576    pub stream: Option<String>,
1577}
1578
1579#[derive(Clone, Debug, Default, Eq, PartialEq)]
1580/// `ObserveEventFrameOptions` data container.
1581pub struct ObserveEventFrameOptions {
1582    /// `stream` field for stream.
1583    pub stream: Option<String>,
1584}
1585
1586impl ObserveEventFrameOptions {
1587    /// Updates or reads `with_stream`.
1588    pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1589        self.stream = Some(stream.into());
1590        self
1591    }
1592}
1593
1594/// Creates or computes `observe_event_frame`.
1595pub fn observe_event_frame<T>(
1596    observe_seq: u64,
1597    path: impl Into<String>,
1598    change: T,
1599    opts: ObserveEventFrameOptions,
1600) -> StorageResult<ObserveEventFrame<T>> {
1601    let envelope = envelope_change(
1602        change,
1603        ChangeEnvelopeOptions::new("observe-event").with_seq(observe_seq),
1604    )?;
1605    Ok(ObserveEventFrame {
1606        lifecycle: envelope.lifecycle,
1607        structure: envelope.structure,
1608        version: envelope.version,
1609        t_ns: envelope.t_ns,
1610        seq: envelope.seq,
1611        change: envelope.change,
1612        observe_seq,
1613        path: path.into(),
1614        stream: opts.stream,
1615    })
1616}
1617
1618/// Creates or computes `assert_observe_event_frame`.
1619pub fn assert_observe_event_frame<T>(
1620    value: &ObserveEventFrame<T>,
1621) -> crate::json::JsonCodecResult<()> {
1622    assert_change_envelope(&ChangeEnvelope {
1623        lifecycle: value.lifecycle.clone(),
1624        structure: value.structure.clone(),
1625        version: value.version.clone(),
1626        t_ns: value.t_ns.clone(),
1627        seq: value.seq,
1628        change: (),
1629    })?;
1630    if value.structure != "observe-event" {
1631        return Err(JsonCodecError::validation(
1632            "observeEventFrameCodec: structure must be observe-event",
1633        ));
1634    }
1635    Ok(())
1636}
1637
1638#[derive(Clone, Debug, Default)]
1639/// `ObserveEventFrameCodec` data container.
1640pub struct ObserveEventFrameCodec<T> {
1641    marker: std::marker::PhantomData<T>,
1642}
1643
1644/// Creates or computes `observe_event_frame_codec`.
1645pub fn observe_event_frame_codec<T>() -> ObserveEventFrameCodec<T> {
1646    ObserveEventFrameCodec {
1647        marker: std::marker::PhantomData,
1648    }
1649}
1650
1651impl<T> Codec<ObserveEventFrame<T>> for ObserveEventFrameCodec<T>
1652where
1653    T: Serialize + DeserializeOwned,
1654{
1655    fn encode(&self, value: &ObserveEventFrame<T>) -> crate::json::JsonCodecResult<Vec<u8>> {
1656        assert_observe_event_frame(value)?;
1657        let value =
1658            serde_json::to_value(value).map_err(|err| JsonCodecError::encode(err.to_string()))?;
1659        strict_canonical_json_bytes(&value)
1660    }
1661
1662    fn decode(&self, bytes: &[u8]) -> crate::json::JsonCodecResult<ObserveEventFrame<T>> {
1663        let value = strict_json_decode(bytes)?;
1664        assert_observe_event_frame_value(&value)?;
1665        serde_json::from_value(value).map_err(|err| JsonCodecError::decode(err.to_string()))
1666    }
1667}
1668
1669fn assert_observe_event_frame_value(value: &Value) -> crate::json::JsonCodecResult<()> {
1670    assert_change_envelope_value(value, "observeEventFrameCodec")?;
1671    let Some(record) = value.as_object() else {
1672        return Err(JsonCodecError::validation(
1673            "observeEventFrameCodec: frame must be an object",
1674        ));
1675    };
1676    if record.get("structure").and_then(Value::as_str) != Some("observe-event") {
1677        return Err(JsonCodecError::validation(
1678            "observeEventFrameCodec: structure must be observe-event",
1679        ));
1680    }
1681    if record.get("observeSeq").and_then(Value::as_u64).is_none() {
1682        return Err(JsonCodecError::validation(
1683            "observeEventFrameCodec: observeSeq must be a non-negative integer",
1684        ));
1685    }
1686    if record.get("path").and_then(Value::as_str).is_none() {
1687        return Err(JsonCodecError::validation(
1688            "observeEventFrameCodec: path must be a string",
1689        ));
1690    }
1691    if record
1692        .get("stream")
1693        .is_some_and(|stream| !stream.is_string())
1694    {
1695        return Err(JsonCodecError::validation(
1696            "observeEventFrameCodec: stream must be a string when present",
1697        ));
1698    }
1699    Ok(())
1700}
1701
1702/// `ObserveEventLogPage` type alias.
1703pub type ObserveEventLogPage<T> = AppendLogPage<ObserveEventFrame<T>>;
1704
1705/// Creates or computes `read_observe_event_log_page`.
1706pub fn read_observe_event_log_page<T: Clone>(
1707    log: &dyn AppendLogStorageTier<ObserveEventFrame<T>>,
1708    opts: AppendLogReadOptions,
1709) -> StorageResult<ObserveEventLogPage<T>> {
1710    read_append_log_page(log, opts)
1711}
1712
1713/// `constant` constant.
1714pub const APPEND_LOG_SEQ_PAD: usize = 20;
1715
1716#[derive(Clone, Debug, PartialEq)]
1717/// `AppendLogEntry` data container.
1718pub struct AppendLogEntry<T> {
1719    /// `key` field for key.
1720    pub key: String,
1721    /// `seq` field for seq.
1722    pub seq: u64,
1723    /// `value` field for value.
1724    pub value: T,
1725}
1726
1727#[derive(Clone, Debug, Default, Eq, PartialEq)]
1728/// `AppendLogReadOptions` data container.
1729pub struct AppendLogReadOptions {
1730    /// `after` field for after.
1731    pub after: Option<u64>,
1732    /// `limit` field for limit.
1733    pub limit: Option<usize>,
1734}
1735
1736#[derive(Clone, Debug, PartialEq)]
1737/// `AppendLogPage` data container.
1738pub struct AppendLogPage<T> {
1739    /// `entries` field for entries.
1740    pub entries: Vec<AppendLogEntry<T>>,
1741    /// `next_after` field for next after.
1742    pub next_after: Option<u64>,
1743    /// `done` field for done.
1744    pub done: bool,
1745}
1746
1747/// `AppendLogStorageTier` behavior contract.
1748pub trait AppendLogStorageTier<T: Clone> {
1749    /// Updates or reads `append`.
1750    fn append(&self, value: T) -> StorageResult<AppendLogEntry<T>>;
1751    /// Updates or reads `read`.
1752    fn read(&self, opts: AppendLogReadOptions) -> StorageResult<Vec<AppendLogEntry<T>>>;
1753    /// Updates or reads `truncate_after`.
1754    fn truncate_after(&self, seq: u64) -> StorageResult<()>;
1755    /// Updates or reads `size`.
1756    fn size(&self) -> StorageResult<usize>;
1757}
1758
1759#[derive(Clone)]
1760/// `AppendLogStorage` data container.
1761pub struct AppendLogStorage<T: Clone> {
1762    kv: Rc<dyn KvStorageTier<T>>,
1763    prefix: String,
1764}
1765
1766impl<T: Clone> fmt::Debug for AppendLogStorage<T> {
1767    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1768        f.debug_struct("AppendLogStorage")
1769            .field("prefix", &self.prefix)
1770            .finish_non_exhaustive()
1771    }
1772}
1773
1774#[derive(Clone)]
1775/// `MultiWriterAppendLogStorage` data container.
1776pub struct MultiWriterAppendLogStorage<T: Clone> {
1777    kv: Rc<dyn KvStorageTier<T>>,
1778    prefix: String,
1779    max_attempts: usize,
1780}
1781
1782impl<T: Clone> fmt::Debug for MultiWriterAppendLogStorage<T> {
1783    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1784        f.debug_struct("MultiWriterAppendLogStorage")
1785            .field("prefix", &self.prefix)
1786            .field("max_attempts", &self.max_attempts)
1787            .finish_non_exhaustive()
1788    }
1789}
1790
1791/// Creates or computes `append_log_key`.
1792pub fn append_log_key(prefix: &str, seq: u64) -> String {
1793    format!("{prefix}/{seq:0APPEND_LOG_SEQ_PAD$}")
1794}
1795
1796/// Creates or computes `append_log_storage`.
1797pub fn append_log_storage<T: Clone>(
1798    kv: Rc<dyn KvStorageTier<T>>,
1799    prefix: impl Into<String>,
1800) -> AppendLogStorage<T> {
1801    AppendLogStorage {
1802        kv,
1803        prefix: prefix.into(),
1804    }
1805}
1806
1807/// Creates or computes `memory_append_log`.
1808pub fn memory_append_log<T: Clone + 'static>(prefix: impl Into<String>) -> AppendLogStorage<T> {
1809    append_log_storage(Rc::new(memory_kv()), prefix)
1810}
1811
1812/// Creates or computes `multi_writer_append_log_storage`.
1813pub fn multi_writer_append_log_storage<T: Clone>(
1814    kv: Rc<dyn KvStorageTier<T>>,
1815    prefix: impl Into<String>,
1816    max_attempts: usize,
1817) -> StorageResult<MultiWriterAppendLogStorage<T>> {
1818    if max_attempts == 0 {
1819        return Err(StorageError::backend(
1820            "multi_writer_append_log_storage: max_attempts must be positive",
1821        ));
1822    }
1823    Ok(MultiWriterAppendLogStorage {
1824        kv,
1825        prefix: prefix.into(),
1826        max_attempts,
1827    })
1828}
1829
1830/// Creates or computes `memory_multi_writer_append_log`.
1831pub fn memory_multi_writer_append_log<T: Clone + 'static>(
1832    prefix: impl Into<String>,
1833) -> MultiWriterAppendLogStorage<T> {
1834    multi_writer_append_log_storage(Rc::new(memory_kv()), prefix, 1024)
1835        .expect("memory_kv supports put-if-absent")
1836}
1837
1838/// Creates or computes `read_append_log_page`.
1839pub fn read_append_log_page<T: Clone>(
1840    log: &dyn AppendLogStorageTier<T>,
1841    opts: AppendLogReadOptions,
1842) -> StorageResult<AppendLogPage<T>> {
1843    let limit = opts.limit.unwrap_or(100);
1844    if limit == 0 {
1845        return Err(StorageError::backend(
1846            "read_append_log_page: limit must be positive",
1847        ));
1848    }
1849    if limit == usize::MAX {
1850        return Err(StorageError::backend(
1851            "read_append_log_page: limit must leave room for one lookahead entry",
1852        ));
1853    }
1854    let mut lookahead_opts = opts.clone();
1855    lookahead_opts.limit = Some(limit + 1);
1856    let mut entries = log.read(lookahead_opts)?;
1857    let done = entries.len() <= limit;
1858    if entries.len() > limit {
1859        entries.truncate(limit);
1860    }
1861    let next_after = entries.last().map(|entry| entry.seq).or(opts.after);
1862    Ok(AppendLogPage {
1863        entries,
1864        next_after,
1865        done,
1866    })
1867}
1868
1869/// `constant` constant.
1870pub const REACTIVE_COLLECTION_SNAPSHOT_FORMAT: &str = "graphrefly.reactive-collection.snapshot.v1";
1871/// `constant` constant.
1872pub const REACTIVE_COLLECTION_CHANGE_FORMAT: &str = "graphrefly.reactive-collection.change.v1";
1873/// `constant` constant.
1874pub const REACTIVE_COLLECTION_FRAME_VERSION: u8 = 1;
1875
1876#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1877/// `ReactiveCollectionKind` variants.
1878pub enum ReactiveCollectionKind {
1879    #[serde(rename = "reactiveList")]
1880    /// `ReactiveList` variant.
1881    ReactiveList,
1882    #[serde(rename = "reactiveLog")]
1883    /// `ReactiveLog` variant.
1884    ReactiveLog,
1885    #[serde(rename = "reactiveMap")]
1886    /// `ReactiveMap` variant.
1887    ReactiveMap,
1888    #[serde(rename = "reactiveIndex")]
1889    /// `ReactiveIndex` variant.
1890    ReactiveIndex,
1891}
1892
1893#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1894#[serde(deny_unknown_fields)]
1895/// `ReactiveCollectionSnapshotFrame` data container.
1896pub struct ReactiveCollectionSnapshotFrame {
1897    /// `format` field for format.
1898    pub format: String,
1899    /// `version` field for version.
1900    pub version: u8,
1901    /// `kind` field for kind.
1902    pub kind: ReactiveCollectionKind,
1903    #[serde(rename = "changeCursor")]
1904    /// `change_cursor` field for change cursor.
1905    pub change_cursor: i64,
1906    /// `snapshot` field for snapshot.
1907    pub snapshot: Value,
1908}
1909
1910#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1911#[serde(deny_unknown_fields)]
1912/// `ReactiveCollectionChangeFrame` data container.
1913pub struct ReactiveCollectionChangeFrame {
1914    /// `format` field for format.
1915    pub format: String,
1916    /// `version` field for version.
1917    pub version: u8,
1918    /// `kind` field for kind.
1919    pub kind: ReactiveCollectionKind,
1920    /// `change` field for change.
1921    pub change: Value,
1922}
1923
1924#[derive(Clone, Debug, PartialEq)]
1925/// `ReactiveCollectionRestoreState` data container.
1926pub struct ReactiveCollectionRestoreState<T> {
1927    /// `kind` field for kind.
1928    pub kind: ReactiveCollectionKind,
1929    /// `state` field for state.
1930    pub state: T,
1931    /// `source` field for source.
1932    pub source: ReactiveCollectionRestoreSource,
1933    /// `snapshot` field for snapshot.
1934    pub snapshot: ReactiveCollectionSnapshotRestoreMeta,
1935    /// `changes` field for changes.
1936    pub changes: ReactiveCollectionChangesRestoreMeta,
1937    /// `cursor` field for cursor.
1938    pub cursor: Option<u64>,
1939    /// `snapshot_found` field for snapshot found.
1940    pub snapshot_found: bool,
1941    /// `changes_applied` field for changes applied.
1942    pub changes_applied: usize,
1943}
1944
1945/// `ReactiveListRestoreState` type alias.
1946pub type ReactiveListRestoreState<T> = ReactiveCollectionRestoreState<Vec<T>>;
1947/// `ReactiveLogRestoreState` type alias.
1948pub type ReactiveLogRestoreState<T> = ReactiveCollectionRestoreState<Vec<T>>;
1949/// `ReactiveMapRestoreState` type alias.
1950pub type ReactiveMapRestoreState<K, V> = ReactiveCollectionRestoreState<Vec<(K, V)>>;
1951/// `ReactiveIndexRestoreState` type alias.
1952pub type ReactiveIndexRestoreState<K, S, V> =
1953    ReactiveCollectionRestoreState<Vec<IndexRow<K, S, V>>>;
1954
1955struct FoldedCollectionState<T> {
1956    state: T,
1957    cursor: Option<u64>,
1958    snapshot_cursor: i64,
1959}
1960
1961#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1962/// `ReactiveCollectionRestoreSource` variants.
1963pub enum ReactiveCollectionRestoreSource {
1964    /// `Empty` variant.
1965    Empty,
1966    /// `Changes` variant.
1967    Changes,
1968    /// `Snapshot` variant.
1969    Snapshot,
1970    /// `SnapshotAndChanges` variant.
1971    SnapshotAndChanges,
1972}
1973
1974#[derive(Clone, Debug, Eq, PartialEq)]
1975/// `ReactiveCollectionSnapshotRestoreMeta` data container.
1976pub struct ReactiveCollectionSnapshotRestoreMeta {
1977    /// `found` field for found.
1978    pub found: bool,
1979    /// `change_cursor` field for change cursor.
1980    pub change_cursor: i64,
1981}
1982
1983#[derive(Clone, Debug, Eq, PartialEq)]
1984/// `ReactiveCollectionChangesRestoreMeta` data container.
1985pub struct ReactiveCollectionChangesRestoreMeta {
1986    /// `applied` field for applied.
1987    pub applied: usize,
1988    /// `cursor` field for cursor.
1989    pub cursor: i64,
1990}
1991
1992#[derive(Clone, Default)]
1993/// `LoadReactiveCollectionStateOptions` data container.
1994pub struct LoadReactiveCollectionStateOptions<'a> {
1995    /// `storage_prefix` field for storage prefix.
1996    pub storage_prefix: Option<&'a str>,
1997    /// `snapshot_key` field for snapshot key.
1998    pub snapshot_key: Option<&'a str>,
1999    /// `change_log` field for change log.
2000    pub change_log: Option<&'a dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>,
2001}
2002
2003#[derive(Clone, Copy, Debug, Default)]
2004/// `ReactiveCollectionSnapshotFrameCodec` data container.
2005pub struct ReactiveCollectionSnapshotFrameCodec;
2006
2007#[derive(Clone, Copy, Debug, Default)]
2008/// `ReactiveCollectionChangeFrameCodec` data container.
2009pub struct ReactiveCollectionChangeFrameCodec;
2010
2011/// Creates or computes `reactive_collection_snapshot_key`.
2012pub fn reactive_collection_snapshot_key(prefix: &str) -> StorageResult<String> {
2013    if prefix.is_empty() {
2014        return Err(StorageError::backend(
2015            "reactive_collection_snapshot_key: storage_prefix must be non-empty",
2016        ));
2017    }
2018    Ok(format!("{prefix}/snapshot"))
2019}
2020
2021/// Creates or computes `reactive_collection_snapshot_frame`.
2022pub fn reactive_collection_snapshot_frame(
2023    kind: ReactiveCollectionKind,
2024    change_cursor: i64,
2025    snapshot: Value,
2026) -> StorageResult<ReactiveCollectionSnapshotFrame> {
2027    let frame = ReactiveCollectionSnapshotFrame {
2028        format: REACTIVE_COLLECTION_SNAPSHOT_FORMAT.to_owned(),
2029        version: REACTIVE_COLLECTION_FRAME_VERSION,
2030        kind,
2031        change_cursor,
2032        snapshot,
2033    };
2034    assert_reactive_collection_snapshot_frame(&frame)?;
2035    Ok(frame)
2036}
2037
2038/// Creates or computes `reactive_collection_change_frame`.
2039pub fn reactive_collection_change_frame(
2040    kind: ReactiveCollectionKind,
2041    change: Value,
2042) -> StorageResult<ReactiveCollectionChangeFrame> {
2043    let frame = ReactiveCollectionChangeFrame {
2044        format: REACTIVE_COLLECTION_CHANGE_FORMAT.to_owned(),
2045        version: REACTIVE_COLLECTION_FRAME_VERSION,
2046        kind,
2047        change,
2048    };
2049    assert_reactive_collection_change_frame(&frame)?;
2050    Ok(frame)
2051}
2052
2053/// Creates or computes `reactive_collection_snapshot_frame_codec`.
2054pub fn reactive_collection_snapshot_frame_codec() -> ReactiveCollectionSnapshotFrameCodec {
2055    ReactiveCollectionSnapshotFrameCodec
2056}
2057
2058/// Creates or computes `reactive_collection_change_frame_codec`.
2059pub fn reactive_collection_change_frame_codec() -> ReactiveCollectionChangeFrameCodec {
2060    ReactiveCollectionChangeFrameCodec
2061}
2062
2063/// Creates or computes `assert_reactive_collection_snapshot_frame`.
2064pub fn assert_reactive_collection_snapshot_frame(
2065    frame: &ReactiveCollectionSnapshotFrame,
2066) -> StorageResult<()> {
2067    if frame.format != REACTIVE_COLLECTION_SNAPSHOT_FORMAT {
2068        return Err(StorageError::backend(format!(
2069            "reactiveCollection snapshot frame: unsupported format {}",
2070            frame.format
2071        )));
2072    }
2073    if frame.version != REACTIVE_COLLECTION_FRAME_VERSION {
2074        return Err(StorageError::backend(format!(
2075            "reactiveCollection snapshot frame: unsupported version {}",
2076            frame.version
2077        )));
2078    }
2079    if frame.change_cursor < -1 {
2080        return Err(StorageError::backend(
2081            "reactiveCollection snapshot frame: changeCursor must be -1 or a non-negative integer",
2082        ));
2083    }
2084    let value = serde_json::to_value(frame).map_err(storage_serde_json_error)?;
2085    strict_canonical_json_bytes(&value).map_err(storage_json_error)?;
2086    Ok(())
2087}
2088
2089/// Creates or computes `assert_reactive_collection_change_frame`.
2090pub fn assert_reactive_collection_change_frame(
2091    frame: &ReactiveCollectionChangeFrame,
2092) -> StorageResult<()> {
2093    if frame.format != REACTIVE_COLLECTION_CHANGE_FORMAT {
2094        return Err(StorageError::backend(format!(
2095            "reactiveCollection change frame: unsupported format {}",
2096            frame.format
2097        )));
2098    }
2099    if frame.version != REACTIVE_COLLECTION_FRAME_VERSION {
2100        return Err(StorageError::backend(format!(
2101            "reactiveCollection change frame: unsupported version {}",
2102            frame.version
2103        )));
2104    }
2105    let value = serde_json::to_value(frame).map_err(storage_serde_json_error)?;
2106    strict_canonical_json_bytes(&value).map_err(storage_json_error)?;
2107    Ok(())
2108}
2109
2110impl Codec<ReactiveCollectionSnapshotFrame> for ReactiveCollectionSnapshotFrameCodec {
2111    fn encode(
2112        &self,
2113        value: &ReactiveCollectionSnapshotFrame,
2114    ) -> crate::json::JsonCodecResult<Vec<u8>> {
2115        assert_reactive_collection_snapshot_frame(value).map_err(storage_error_to_json)?;
2116        let json =
2117            serde_json::to_value(value).map_err(|err| JsonCodecError::encode(err.to_string()))?;
2118        strict_canonical_json_bytes(&json)
2119    }
2120
2121    fn decode(
2122        &self,
2123        bytes: &[u8],
2124    ) -> crate::json::JsonCodecResult<ReactiveCollectionSnapshotFrame> {
2125        let json = strict_json_decode(bytes)?;
2126        let frame =
2127            serde_json::from_value(json).map_err(|err| JsonCodecError::decode(err.to_string()))?;
2128        assert_reactive_collection_snapshot_frame(&frame).map_err(storage_error_to_json)?;
2129        Ok(frame)
2130    }
2131}
2132
2133impl Codec<ReactiveCollectionChangeFrame> for ReactiveCollectionChangeFrameCodec {
2134    fn encode(
2135        &self,
2136        value: &ReactiveCollectionChangeFrame,
2137    ) -> crate::json::JsonCodecResult<Vec<u8>> {
2138        assert_reactive_collection_change_frame(value).map_err(storage_error_to_json)?;
2139        let json =
2140            serde_json::to_value(value).map_err(|err| JsonCodecError::encode(err.to_string()))?;
2141        strict_canonical_json_bytes(&json)
2142    }
2143
2144    fn decode(&self, bytes: &[u8]) -> crate::json::JsonCodecResult<ReactiveCollectionChangeFrame> {
2145        let json = strict_json_decode(bytes)?;
2146        let frame =
2147            serde_json::from_value(json).map_err(|err| JsonCodecError::decode(err.to_string()))?;
2148        assert_reactive_collection_change_frame(&frame).map_err(storage_error_to_json)?;
2149        Ok(frame)
2150    }
2151}
2152
2153/// Creates or computes `load_reactive_list_state`.
2154pub fn load_reactive_list_state<T>(
2155    snapshot_store: &dyn KvStorageTier<ReactiveCollectionSnapshotFrame>,
2156    options: LoadReactiveCollectionStateOptions<'_>,
2157) -> StorageResult<ReactiveListRestoreState<T>>
2158where
2159    T: Clone + Serialize + DeserializeOwned,
2160{
2161    let (snapshot, cursor, snapshot_found) = load_collection_snapshot::<Vec<T>>(
2162        snapshot_store,
2163        &options,
2164        ReactiveCollectionKind::ReactiveList,
2165    )?;
2166    let mut state = snapshot.unwrap_or_default();
2167    let (cursor, changes_applied) = fold_collection_changes(
2168        state,
2169        cursor,
2170        options.change_log,
2171        ReactiveCollectionKind::ReactiveList,
2172        fold_list_change::<T>,
2173    )?;
2174    state = cursor.state;
2175    restore_state(
2176        ReactiveCollectionKind::ReactiveList,
2177        state,
2178        snapshot_found,
2179        cursor.snapshot_cursor,
2180        changes_applied,
2181        cursor.cursor,
2182    )
2183}
2184
2185/// Creates or computes `load_reactive_log_state`.
2186pub fn load_reactive_log_state<T>(
2187    snapshot_store: &dyn KvStorageTier<ReactiveCollectionSnapshotFrame>,
2188    options: LoadReactiveCollectionStateOptions<'_>,
2189) -> StorageResult<ReactiveLogRestoreState<T>>
2190where
2191    T: Clone + Serialize + DeserializeOwned,
2192{
2193    let (snapshot, cursor, snapshot_found) = load_collection_snapshot::<Vec<T>>(
2194        snapshot_store,
2195        &options,
2196        ReactiveCollectionKind::ReactiveLog,
2197    )?;
2198    let mut state = snapshot.unwrap_or_default();
2199    let (cursor, changes_applied) = fold_collection_changes(
2200        state,
2201        cursor,
2202        options.change_log,
2203        ReactiveCollectionKind::ReactiveLog,
2204        fold_log_change::<T>,
2205    )?;
2206    state = cursor.state;
2207    restore_state(
2208        ReactiveCollectionKind::ReactiveLog,
2209        state,
2210        snapshot_found,
2211        cursor.snapshot_cursor,
2212        changes_applied,
2213        cursor.cursor,
2214    )
2215}
2216
2217/// Creates or computes `load_reactive_map_state`.
2218pub fn load_reactive_map_state<K, V>(
2219    snapshot_store: &dyn KvStorageTier<ReactiveCollectionSnapshotFrame>,
2220    options: LoadReactiveCollectionStateOptions<'_>,
2221) -> StorageResult<ReactiveMapRestoreState<K, V>>
2222where
2223    K: Clone + Serialize + DeserializeOwned,
2224    V: Clone + Serialize + DeserializeOwned,
2225{
2226    let (snapshot, cursor, snapshot_found) = load_collection_snapshot::<Vec<(K, V)>>(
2227        snapshot_store,
2228        &options,
2229        ReactiveCollectionKind::ReactiveMap,
2230    )?;
2231    let mut state = snapshot.unwrap_or_default();
2232    assert_unique_map_keys(&state, "reactiveMap snapshot")?;
2233    let (cursor, changes_applied) = fold_collection_changes(
2234        state,
2235        cursor,
2236        options.change_log,
2237        ReactiveCollectionKind::ReactiveMap,
2238        fold_map_change::<K, V>,
2239    )?;
2240    state = cursor.state;
2241    assert_unique_map_keys(&state, "reactiveMap restore")?;
2242    restore_state(
2243        ReactiveCollectionKind::ReactiveMap,
2244        state,
2245        snapshot_found,
2246        cursor.snapshot_cursor,
2247        changes_applied,
2248        cursor.cursor,
2249    )
2250}
2251
2252/// Creates or computes `load_reactive_index_state`.
2253pub fn load_reactive_index_state<K, S, V>(
2254    snapshot_store: &dyn KvStorageTier<ReactiveCollectionSnapshotFrame>,
2255    options: LoadReactiveCollectionStateOptions<'_>,
2256) -> StorageResult<ReactiveIndexRestoreState<K, S, V>>
2257where
2258    K: Clone + Serialize + DeserializeOwned,
2259    S: Clone + Serialize + DeserializeOwned,
2260    V: Clone + Serialize + DeserializeOwned,
2261{
2262    let (snapshot, cursor, snapshot_found) = load_collection_snapshot::<Vec<IndexRow<K, S, V>>>(
2263        snapshot_store,
2264        &options,
2265        ReactiveCollectionKind::ReactiveIndex,
2266    )?;
2267    let mut state = snapshot.unwrap_or_default();
2268    assert_unique_index_primaries(&state, "reactiveIndex snapshot")?;
2269    let (cursor, changes_applied) = fold_collection_changes(
2270        state,
2271        cursor,
2272        options.change_log,
2273        ReactiveCollectionKind::ReactiveIndex,
2274        fold_index_change::<K, S, V>,
2275    )?;
2276    state = cursor.state;
2277    assert_unique_index_primaries(&state, "reactiveIndex restore")?;
2278    restore_state(
2279        ReactiveCollectionKind::ReactiveIndex,
2280        state,
2281        snapshot_found,
2282        cursor.snapshot_cursor,
2283        changes_applied,
2284        cursor.cursor,
2285    )
2286}
2287
2288fn load_collection_snapshot<T>(
2289    snapshot_store: &dyn KvStorageTier<ReactiveCollectionSnapshotFrame>,
2290    options: &LoadReactiveCollectionStateOptions<'_>,
2291    kind: ReactiveCollectionKind,
2292) -> StorageResult<(Option<T>, Option<u64>, bool)>
2293where
2294    T: DeserializeOwned,
2295{
2296    let key = resolve_collection_snapshot_key(options)?;
2297    let Some(frame) = snapshot_store.get(&key)? else {
2298        return Ok((None, None, false));
2299    };
2300    assert_reactive_collection_snapshot_frame(&frame)?;
2301    if frame.kind != kind {
2302        return Err(StorageError::backend(format!(
2303            "reactiveCollection snapshot frame: expected {:?}, got {:?}",
2304            kind, frame.kind
2305        )));
2306    }
2307    let state = serde_json::from_value(frame.snapshot.clone()).map_err(storage_serde_json_error)?;
2308    let cursor = if frame.change_cursor < 0 {
2309        None
2310    } else {
2311        Some(frame.change_cursor as u64)
2312    };
2313    Ok((Some(state), cursor, true))
2314}
2315
2316fn fold_collection_changes<T, F>(
2317    mut state: T,
2318    mut cursor: Option<u64>,
2319    change_log: Option<&dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>,
2320    kind: ReactiveCollectionKind,
2321    mut fold: F,
2322) -> StorageResult<(FoldedCollectionState<T>, usize)>
2323where
2324    F: FnMut(&mut T, ReactiveCollectionChangeFrame) -> StorageResult<()>,
2325{
2326    let snapshot_cursor = cursor
2327        .map(seq_to_snapshot_cursor)
2328        .transpose()?
2329        .unwrap_or(-1);
2330    let Some(log) = change_log else {
2331        return Ok((
2332            FoldedCollectionState {
2333                state,
2334                cursor,
2335                snapshot_cursor,
2336            },
2337            0,
2338        ));
2339    };
2340    let entries = log.read(AppendLogReadOptions {
2341        after: cursor,
2342        limit: None,
2343    })?;
2344    let mut expected = cursor.map_or(0, |seq| seq.saturating_add(1));
2345    for entry in entries.iter() {
2346        if entry.seq != expected {
2347            return Err(StorageError::backend(format!(
2348                "reactiveCollection load: non-contiguous change log sequence, expected {expected}, got {}",
2349                entry.seq
2350            )));
2351        }
2352        assert_reactive_collection_change_frame(&entry.value)?;
2353        if entry.value.kind != kind {
2354            return Err(StorageError::backend(format!(
2355                "reactiveCollection change frame: expected {:?}, got {:?}",
2356                kind, entry.value.kind
2357            )));
2358        }
2359        fold(&mut state, entry.value.clone())?;
2360        cursor = Some(entry.seq);
2361        expected = expected.checked_add(1).ok_or_else(|| {
2362            StorageError::backend("reactiveCollection load: change log sequence overflow")
2363        })?;
2364    }
2365    Ok((
2366        FoldedCollectionState {
2367            state,
2368            cursor,
2369            snapshot_cursor,
2370        },
2371        entries.len(),
2372    ))
2373}
2374
2375fn restore_state<T>(
2376    kind: ReactiveCollectionKind,
2377    state: T,
2378    snapshot_found: bool,
2379    snapshot_cursor: i64,
2380    changes_applied: usize,
2381    cursor: Option<u64>,
2382) -> StorageResult<ReactiveCollectionRestoreState<T>> {
2383    let change_cursor = cursor
2384        .map(seq_to_snapshot_cursor)
2385        .transpose()?
2386        .unwrap_or(-1);
2387    let source = match (snapshot_found, changes_applied > 0) {
2388        (false, false) => ReactiveCollectionRestoreSource::Empty,
2389        (false, true) => ReactiveCollectionRestoreSource::Changes,
2390        (true, false) => ReactiveCollectionRestoreSource::Snapshot,
2391        (true, true) => ReactiveCollectionRestoreSource::SnapshotAndChanges,
2392    };
2393    Ok(ReactiveCollectionRestoreState {
2394        kind,
2395        state,
2396        source,
2397        snapshot: ReactiveCollectionSnapshotRestoreMeta {
2398            found: snapshot_found,
2399            change_cursor: snapshot_cursor,
2400        },
2401        changes: ReactiveCollectionChangesRestoreMeta {
2402            applied: changes_applied,
2403            cursor: change_cursor,
2404        },
2405        cursor,
2406        snapshot_found,
2407        changes_applied,
2408    })
2409}
2410
2411fn fold_list_change<T>(
2412    state: &mut Vec<T>,
2413    frame: ReactiveCollectionChangeFrame,
2414) -> StorageResult<()>
2415where
2416    T: Clone + Serialize + DeserializeOwned,
2417{
2418    let change: ListChange<T> =
2419        serde_json::from_value(frame.change).map_err(storage_serde_json_error)?;
2420    match change {
2421        ListChange::Append { value } => state.push(value),
2422        ListChange::AppendMany { values } => state.extend(values),
2423        ListChange::Insert { index, value } => {
2424            if index > state.len() {
2425                return Err(StorageError::backend(format!(
2426                    "reactiveList fold: insert index {index} is out of bounds for len {}",
2427                    state.len()
2428                )));
2429            }
2430            state.insert(index, value);
2431        }
2432        ListChange::InsertMany { index, values } => {
2433            if index > state.len() {
2434                return Err(StorageError::backend(format!(
2435                    "reactiveList fold: insertMany index {index} is out of bounds for len {}",
2436                    state.len()
2437                )));
2438            }
2439            state.splice(index..index, values);
2440        }
2441        ListChange::Pop { index, value } => {
2442            if index >= state.len() {
2443                return Err(StorageError::backend(format!(
2444                    "reactiveList fold: pop index {index} is out of bounds for len {}",
2445                    state.len()
2446                )));
2447            }
2448            let actual = state.remove(index);
2449            if !strict_json_equal(&actual, &value)? {
2450                return Err(StorageError::backend(
2451                    "reactiveList fold: pop value does not match stored state",
2452                ));
2453            }
2454        }
2455        ListChange::TrimHead { n } => {
2456            if n > state.len() {
2457                return Err(StorageError::backend(format!(
2458                    "reactiveList fold: trimHead {n} exceeds len {}",
2459                    state.len()
2460                )));
2461            }
2462            state.drain(0..n);
2463        }
2464        ListChange::Clear { count } => {
2465            if count != state.len() {
2466                return Err(StorageError::backend(format!(
2467                    "reactiveList fold: clear count {count} does not match len {}",
2468                    state.len()
2469                )));
2470            }
2471            state.clear();
2472        }
2473    }
2474    Ok(())
2475}
2476
2477fn fold_log_change<T>(state: &mut Vec<T>, frame: ReactiveCollectionChangeFrame) -> StorageResult<()>
2478where
2479    T: Clone + Serialize + DeserializeOwned,
2480{
2481    let change: LogChange<T> =
2482        serde_json::from_value(frame.change).map_err(storage_serde_json_error)?;
2483    match change {
2484        LogChange::Append { value } => state.push(value),
2485        LogChange::AppendMany { values } => state.extend(values),
2486        LogChange::TrimHead { n } => {
2487            if n > state.len() {
2488                return Err(StorageError::backend(format!(
2489                    "reactiveLog fold: trimHead {n} exceeds len {}",
2490                    state.len()
2491                )));
2492            }
2493            state.drain(0..n);
2494        }
2495        LogChange::Clear { count } => {
2496            if count != state.len() {
2497                return Err(StorageError::backend(format!(
2498                    "reactiveLog fold: clear count {count} does not match len {}",
2499                    state.len()
2500                )));
2501            }
2502            state.clear();
2503        }
2504    }
2505    Ok(())
2506}
2507
2508fn fold_map_change<K, V>(
2509    state: &mut Vec<(K, V)>,
2510    frame: ReactiveCollectionChangeFrame,
2511) -> StorageResult<()>
2512where
2513    K: Clone + Serialize + DeserializeOwned,
2514    V: Clone + Serialize + DeserializeOwned,
2515{
2516    let change: MapChange<K, V> =
2517        serde_json::from_value(frame.change).map_err(storage_serde_json_error)?;
2518    match change {
2519        MapChange::Set { key, value } => match find_map_key(state, &key)? {
2520            Some(index) => state[index] = (key, value),
2521            None => state.push((key, value)),
2522        },
2523        MapChange::Delete { key, previous } => {
2524            let Some(index) = find_map_key(state, &key)? else {
2525                return Err(StorageError::backend(
2526                    "reactiveMap fold: delete key is missing",
2527                ));
2528            };
2529            let (_, actual) = state.remove(index);
2530            if !strict_json_equal(&actual, &previous)? {
2531                return Err(StorageError::backend(
2532                    "reactiveMap fold: delete previous value does not match stored state",
2533                ));
2534            }
2535        }
2536        MapChange::Clear { count } => {
2537            if count != state.len() {
2538                return Err(StorageError::backend(format!(
2539                    "reactiveMap fold: clear count {count} does not match len {}",
2540                    state.len()
2541                )));
2542            }
2543            state.clear();
2544        }
2545    }
2546    Ok(())
2547}
2548
2549fn fold_index_change<K, S, V>(
2550    state: &mut Vec<IndexRow<K, S, V>>,
2551    frame: ReactiveCollectionChangeFrame,
2552) -> StorageResult<()>
2553where
2554    K: Clone + Serialize + DeserializeOwned,
2555    S: Clone + Serialize + DeserializeOwned,
2556    V: Clone + Serialize + DeserializeOwned,
2557{
2558    let change: IndexChange<K, S, V> =
2559        serde_json::from_value(frame.change).map_err(storage_serde_json_error)?;
2560    match change {
2561        IndexChange::Upsert {
2562            primary,
2563            secondary,
2564            value,
2565        } => {
2566            let row = IndexRow {
2567                primary,
2568                secondary,
2569                value,
2570            };
2571            match find_index_primary(state, &row.primary)? {
2572                Some(index) => state[index] = row,
2573                None => state.push(row),
2574            }
2575        }
2576        IndexChange::Delete { primary } => {
2577            let Some(index) = find_index_primary(state, &primary)? else {
2578                return Err(StorageError::backend(
2579                    "reactiveIndex fold: delete primary is missing",
2580                ));
2581            };
2582            state.remove(index);
2583        }
2584        IndexChange::DeleteMany { primaries } => {
2585            remove_index_primaries(state, &primaries, "reactiveIndex fold: deleteMany primary")?;
2586        }
2587        IndexChange::Clear { count } => {
2588            if count != state.len() {
2589                return Err(StorageError::backend(format!(
2590                    "reactiveIndex fold: clear count {count} does not match len {}",
2591                    state.len()
2592                )));
2593            }
2594            state.clear();
2595        }
2596    }
2597    Ok(())
2598}
2599
2600fn assert_unique_map_keys<K, V>(entries: &[(K, V)], label: &str) -> StorageResult<()>
2601where
2602    K: Serialize,
2603{
2604    let mut seen = Vec::<Vec<u8>>::new();
2605    for (index, (key, _)) in entries.iter().enumerate() {
2606        let id = strict_json_identity(key)?;
2607        if seen.iter().any(|existing| existing == &id) {
2608            return Err(StorageError::backend(format!(
2609                "{label}: entry {index} duplicates an earlier key"
2610            )));
2611        }
2612        seen.push(id);
2613    }
2614    Ok(())
2615}
2616
2617fn assert_unique_index_primaries<K, S, V>(
2618    rows: &[IndexRow<K, S, V>],
2619    label: &str,
2620) -> StorageResult<()>
2621where
2622    K: Serialize,
2623{
2624    let mut seen = Vec::<Vec<u8>>::new();
2625    for (index, row) in rows.iter().enumerate() {
2626        let id = strict_json_identity(&row.primary)?;
2627        if seen.iter().any(|existing| existing == &id) {
2628            return Err(StorageError::backend(format!(
2629                "{label}: row {index} duplicates an earlier primary"
2630            )));
2631        }
2632        seen.push(id);
2633    }
2634    Ok(())
2635}
2636
2637fn find_map_key<K, V>(entries: &[(K, V)], key: &K) -> StorageResult<Option<usize>>
2638where
2639    K: Serialize,
2640{
2641    let target = strict_json_identity(key)?;
2642    for (index, (candidate, _)) in entries.iter().enumerate() {
2643        if strict_json_identity(candidate)? == target {
2644            return Ok(Some(index));
2645        }
2646    }
2647    Ok(None)
2648}
2649
2650fn find_index_primary<K, S, V>(
2651    rows: &[IndexRow<K, S, V>],
2652    primary: &K,
2653) -> StorageResult<Option<usize>>
2654where
2655    K: Serialize,
2656{
2657    let target = strict_json_identity(primary)?;
2658    for (index, row) in rows.iter().enumerate() {
2659        if strict_json_identity(&row.primary)? == target {
2660            return Ok(Some(index));
2661        }
2662    }
2663    Ok(None)
2664}
2665
2666fn remove_index_primaries<K, S, V>(
2667    rows: &mut Vec<IndexRow<K, S, V>>,
2668    primaries: &[K],
2669    label: &str,
2670) -> StorageResult<()>
2671where
2672    K: Serialize,
2673{
2674    let mut seen = Vec::<Vec<u8>>::new();
2675    let mut indexes = Vec::<usize>::new();
2676    for (index, primary) in primaries.iter().enumerate() {
2677        let id = strict_json_identity(primary)?;
2678        if seen.iter().any(|existing| existing == &id) {
2679            return Err(StorageError::backend(format!(
2680                "{label} {index} duplicates an earlier primary"
2681            )));
2682        }
2683        seen.push(id);
2684        let Some(row_index) = find_index_primary(rows, primary)? else {
2685            return Err(StorageError::backend(format!("{label} {index} is missing")));
2686        };
2687        indexes.push(row_index);
2688    }
2689    indexes.sort_unstable_by(|a, b| b.cmp(a));
2690    for index in indexes {
2691        rows.remove(index);
2692    }
2693    Ok(())
2694}
2695
2696fn resolve_collection_snapshot_key(
2697    options: &LoadReactiveCollectionStateOptions<'_>,
2698) -> StorageResult<String> {
2699    if let Some(key) = options.snapshot_key {
2700        if key.is_empty() {
2701            return Err(StorageError::backend(
2702                "reactiveCollection load: snapshot_key must be non-empty",
2703            ));
2704        }
2705        return Ok(key.to_owned());
2706    }
2707    let prefix = options.storage_prefix.ok_or_else(|| {
2708        StorageError::backend("reactiveCollection load: storage_prefix or snapshot_key is required")
2709    })?;
2710    reactive_collection_snapshot_key(prefix)
2711}
2712
2713fn strict_json_equal<T: Serialize>(left: &T, right: &T) -> StorageResult<bool> {
2714    let left = serde_json::to_value(left).map_err(storage_serde_json_error)?;
2715    let right = serde_json::to_value(right).map_err(storage_serde_json_error)?;
2716    Ok(
2717        strict_canonical_json_bytes(&left).map_err(storage_json_error)?
2718            == strict_canonical_json_bytes(&right).map_err(storage_json_error)?,
2719    )
2720}
2721
2722fn strict_json_identity<T: Serialize>(value: &T) -> StorageResult<Vec<u8>> {
2723    let value = serde_json::to_value(value).map_err(storage_serde_json_error)?;
2724    strict_canonical_json_bytes(&value).map_err(storage_json_error)
2725}
2726
2727fn seq_to_snapshot_cursor(seq: u64) -> StorageResult<i64> {
2728    i64::try_from(seq)
2729        .map_err(|_| StorageError::backend("reactiveCollection cursor exceeds i64 range"))
2730}
2731
2732fn storage_error_to_json(error: StorageError) -> JsonCodecError {
2733    JsonCodecError::validation(error.to_string())
2734}
2735
2736fn storage_serde_json_error(error: serde_json::Error) -> StorageError {
2737    StorageError::backend(format!("reactiveCollection JSON error: {error}"))
2738}
2739
2740impl<T: Clone> AppendLogStorage<T> {
2741    fn next_seq(&self) -> StorageResult<u64> {
2742        next_seq_from_keys(&self.prefix, self.kv.list(&format!("{}/", self.prefix))?)
2743    }
2744}
2745
2746impl<T: Clone> AppendLogStorageTier<T> for AppendLogStorage<T> {
2747    fn append(&self, value: T) -> StorageResult<AppendLogEntry<T>> {
2748        let seq = self.next_seq()?;
2749        let key = append_log_key(&self.prefix, seq);
2750        self.kv.set(&key, value.clone())?;
2751        Ok(AppendLogEntry { key, seq, value })
2752    }
2753
2754    fn read(&self, opts: AppendLogReadOptions) -> StorageResult<Vec<AppendLogEntry<T>>> {
2755        read_append_log_entries(self.kv.as_ref(), &self.prefix, opts)
2756    }
2757
2758    fn truncate_after(&self, seq: u64) -> StorageResult<()> {
2759        delete_append_log_entries_after(self.kv.as_ref(), &self.prefix, seq)
2760    }
2761
2762    fn size(&self) -> StorageResult<usize> {
2763        size_from_keys(&self.prefix, &self.kv.list(&format!("{}/", self.prefix))?)
2764    }
2765}
2766
2767impl<T: Clone> AppendLogStorageTier<T> for MultiWriterAppendLogStorage<T> {
2768    fn append(&self, value: T) -> StorageResult<AppendLogEntry<T>> {
2769        let mut seq =
2770            next_seq_from_keys(&self.prefix, self.kv.list(&format!("{}/", self.prefix))?)?;
2771        let mut attempts = 0;
2772        loop {
2773            if attempts >= self.max_attempts {
2774                let refreshed =
2775                    next_seq_from_keys(&self.prefix, self.kv.list(&format!("{}/", self.prefix))?)?;
2776                seq = seq.max(refreshed);
2777                attempts = 0;
2778            }
2779            attempts += 1;
2780            let key = append_log_key(&self.prefix, seq);
2781            if self.kv.put_if_absent(&key, value.clone())? {
2782                return Ok(AppendLogEntry { key, seq, value });
2783            }
2784            seq = seq.checked_add(1).ok_or_else(|| {
2785                StorageError::backend(format!(
2786                    "append log next sequence is outside the u64 range: {}",
2787                    self.prefix
2788                ))
2789            })?;
2790        }
2791    }
2792
2793    fn read(&self, opts: AppendLogReadOptions) -> StorageResult<Vec<AppendLogEntry<T>>> {
2794        read_append_log_entries(self.kv.as_ref(), &self.prefix, opts)
2795    }
2796
2797    fn truncate_after(&self, _seq: u64) -> StorageResult<()> {
2798        Err(StorageError::backend(
2799            "multi_writer_append_log_storage.truncate_after: unsupported without a stronger compaction capability",
2800        ))
2801    }
2802
2803    fn size(&self) -> StorageResult<usize> {
2804        size_from_keys(&self.prefix, &self.kv.list(&format!("{}/", self.prefix))?)
2805    }
2806}
2807
2808fn read_append_log_entries<T: Clone>(
2809    kv: &dyn KvStorageTier<T>,
2810    prefix: &str,
2811    opts: AppendLogReadOptions,
2812) -> StorageResult<Vec<AppendLogEntry<T>>> {
2813    let mut keys = kv
2814        .list(&format!("{prefix}/"))?
2815        .into_iter()
2816        .map(|key| seq_from_key(prefix, &key).map(|seq| (key, seq)))
2817        .collect::<StorageResult<Vec<_>>>()?;
2818    keys.retain(|(_, seq)| opts.after.is_none_or(|after| *seq > after));
2819    keys.sort_by_key(|(_, seq)| *seq);
2820    if let Some(limit) = opts.limit {
2821        keys.truncate(limit);
2822    }
2823    let mut entries = Vec::with_capacity(keys.len());
2824    for (key, seq) in keys {
2825        let value = kv.get(&key)?.ok_or_else(|| {
2826            StorageError::backend(format!("append log listed key is missing: {key}"))
2827        })?;
2828        entries.push(AppendLogEntry { key, seq, value });
2829    }
2830    Ok(entries)
2831}
2832
2833fn delete_append_log_entries_after<T: Clone>(
2834    kv: &dyn KvStorageTier<T>,
2835    prefix: &str,
2836    seq: u64,
2837) -> StorageResult<()> {
2838    let keys = kv
2839        .list(&format!("{prefix}/"))?
2840        .into_iter()
2841        .map(|key| seq_from_key(prefix, &key).map(|parsed| (key, parsed)))
2842        .collect::<StorageResult<Vec<_>>>()?;
2843    for (key, parsed) in keys {
2844        if parsed > seq {
2845            kv.delete(&key)?;
2846        }
2847    }
2848    Ok(())
2849}
2850
2851fn next_seq_from_keys(prefix: &str, keys: Vec<String>) -> StorageResult<u64> {
2852    let Some(max_seq) = keys
2853        .iter()
2854        .map(|key| seq_from_key(prefix, key))
2855        .collect::<StorageResult<Vec<_>>>()?
2856        .into_iter()
2857        .max()
2858    else {
2859        return Ok(0);
2860    };
2861    max_seq.checked_add(1).ok_or_else(|| {
2862        StorageError::backend(format!(
2863            "append log next sequence is outside the u64 range: {prefix}"
2864        ))
2865    })
2866}
2867
2868fn size_from_keys(prefix: &str, keys: &[String]) -> StorageResult<usize> {
2869    for key in keys {
2870        seq_from_key(prefix, key)?;
2871    }
2872    Ok(keys.len())
2873}
2874
2875fn seq_from_key(prefix: &str, key: &str) -> StorageResult<u64> {
2876    let head = format!("{prefix}/");
2877    let raw = key
2878        .strip_prefix(&head)
2879        .ok_or_else(|| StorageError::backend(format!("append log key outside prefix: {key}")))?;
2880    if !raw.bytes().all(|byte| byte.is_ascii_digit()) {
2881        return Err(StorageError::backend(format!(
2882            "append log key has a non-numeric sequence: {key}"
2883        )));
2884    }
2885    if raw.len() != APPEND_LOG_SEQ_PAD {
2886        return Err(StorageError::backend(format!(
2887            "append log key sequence must be {APPEND_LOG_SEQ_PAD} padded digits: {key}"
2888        )));
2889    }
2890    raw.parse::<u64>().map_err(|_| {
2891        StorageError::backend(format!(
2892            "append log key sequence is outside the u64 range: {key}"
2893        ))
2894    })
2895}
2896
2897#[derive(Clone, Debug, Eq, PartialEq)]
2898/// `ReadThroughLookupTier` data container.
2899pub struct ReadThroughLookupTier {
2900    /// `index` field for index.
2901    pub index: isize,
2902    /// `name` field for name.
2903    pub name: Option<String>,
2904}
2905
2906#[derive(Clone, Debug, Eq, PartialEq)]
2907/// `ReadThroughOutcome` variants.
2908pub enum ReadThroughOutcome {
2909    /// `Hit` variant.
2910    Hit,
2911    /// `Miss` variant.
2912    Miss,
2913    /// `Error` variant.
2914    Error,
2915}
2916
2917#[derive(Clone, Debug)]
2918/// `ReadThroughLookupFact` data container.
2919pub struct ReadThroughLookupFact<T> {
2920    /// `outcome` field for outcome.
2921    pub outcome: ReadThroughOutcome,
2922    /// `key` field for key.
2923    pub key: String,
2924    /// `tier` field for tier.
2925    pub tier: ReadThroughLookupTier,
2926    /// `value` field for value.
2927    pub value: Option<T>,
2928    /// `generation` field for generation.
2929    pub generation: Option<KvGeneration>,
2930    /// `error` field for error.
2931    pub error: Option<StorageError>,
2932}
2933
2934#[derive(Clone, Debug, PartialEq)]
2935/// `ReadThroughPromotionFact` data container.
2936pub struct ReadThroughPromotionFact {
2937    /// `tier` field for tier.
2938    pub tier: ReadThroughLookupTier,
2939    /// `ok` field for ok.
2940    pub ok: bool,
2941    /// `error` field for error.
2942    pub error: Option<StorageError>,
2943}
2944
2945#[derive(Clone, Debug, Eq, PartialEq)]
2946/// `TieredReadThroughStatus` variants.
2947pub enum TieredReadThroughStatus {
2948    /// `Hit` variant.
2949    Hit,
2950    /// `Miss` variant.
2951    Miss,
2952    /// `Error` variant.
2953    Error,
2954}
2955
2956#[derive(Clone, Debug)]
2957/// `TieredReadThroughResult` data container.
2958pub struct TieredReadThroughResult<T> {
2959    /// `status` field for status.
2960    pub status: TieredReadThroughStatus,
2961    /// `key` field for key.
2962    pub key: String,
2963    /// `value` field for value.
2964    pub value: Option<T>,
2965    /// `hit_tier` field for hit tier.
2966    pub hit_tier: Option<ReadThroughLookupTier>,
2967    /// `facts` field for facts.
2968    pub facts: Vec<ReadThroughLookupFact<T>>,
2969    /// `promotions` field for promotions.
2970    pub promotions: Vec<ReadThroughPromotionFact>,
2971}
2972
2973#[derive(Clone, Debug, Default, Eq, PartialEq)]
2974/// `PromotionPolicy` variants.
2975pub enum PromotionPolicy {
2976    #[default]
2977    /// `AllEarlier` variant.
2978    AllEarlier,
2979    /// `Disabled` variant.
2980    Disabled,
2981    /// `Indices` variant.
2982    Indices(Vec<usize>),
2983}
2984
2985/// `ReadThroughMissContext` data container.
2986pub struct ReadThroughMissContext {
2987    /// `key` field for key.
2988    pub key: String,
2989    /// `tier` field for tier.
2990    pub tier: ReadThroughLookupTier,
2991}
2992
2993/// `ReadThroughErrorContext` data container.
2994pub struct ReadThroughErrorContext {
2995    /// `key` field for key.
2996    pub key: String,
2997    /// `tier` field for tier.
2998    pub tier: ReadThroughLookupTier,
2999    /// `stage` field for stage.
3000    pub stage: ReadThroughErrorStage,
3001    /// `error` field for error.
3002    pub error: StorageError,
3003}
3004
3005#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3006/// `ReadThroughErrorStage` variants.
3007pub enum ReadThroughErrorStage {
3008    /// `Lookup` variant.
3009    Lookup,
3010    /// `Promotion` variant.
3011    Promotion,
3012}
3013
3014/// `ReadThroughLoadFn` type alias.
3015pub type ReadThroughLoadFn<'a, T> = dyn Fn(&str) -> StorageResult<Option<T>> + 'a;
3016/// `ReadThroughMissFn` type alias.
3017pub type ReadThroughMissFn<'a> = dyn Fn(ReadThroughMissContext) + 'a;
3018/// `ReadThroughErrorFn` type alias.
3019pub type ReadThroughErrorFn<'a> = dyn Fn(ReadThroughErrorContext) + 'a;
3020
3021/// `TieredReadThroughOptions` data container.
3022pub struct TieredReadThroughOptions<'a, T: Clone> {
3023    /// `key` field for key.
3024    pub key: String,
3025    /// `tiers` field for tiers.
3026    pub tiers: Vec<&'a dyn KvStorageTier<T>>,
3027    /// `tier_names` field for tier names.
3028    pub tier_names: Vec<String>,
3029    /// `load` field for load.
3030    pub load: Option<Box<ReadThroughLoadFn<'a, T>>>,
3031    /// `promote_to` field for promote to.
3032    pub promote_to: PromotionPolicy,
3033    /// `on_miss` field for on miss.
3034    pub on_miss: Option<Box<ReadThroughMissFn<'a>>>,
3035    /// `on_error` field for on error.
3036    pub on_error: Option<Box<ReadThroughErrorFn<'a>>>,
3037}
3038
3039impl<'a, T: Clone> TieredReadThroughOptions<'a, T> {
3040    /// Creates or computes `new`.
3041    pub fn new(key: impl Into<String>, tiers: Vec<&'a dyn KvStorageTier<T>>) -> Self {
3042        Self {
3043            key: key.into(),
3044            tiers,
3045            tier_names: Vec::new(),
3046            load: None,
3047            promote_to: PromotionPolicy::AllEarlier,
3048            on_miss: None,
3049            on_error: None,
3050        }
3051    }
3052}
3053
3054/// Creates or computes `read_through_kv`.
3055pub fn read_through_kv<T: Clone>(
3056    opts: TieredReadThroughOptions<'_, T>,
3057) -> TieredReadThroughResult<T> {
3058    tiered_read_through(opts)
3059}
3060
3061/// Graph-agnostic tiered lookup + optional promotion helper (D104/D123).
3062pub fn tiered_read_through<T: Clone>(
3063    opts: TieredReadThroughOptions<'_, T>,
3064) -> TieredReadThroughResult<T> {
3065    let TieredReadThroughOptions {
3066        key,
3067        tiers,
3068        tier_names,
3069        load,
3070        promote_to,
3071        on_miss,
3072        on_error,
3073    } = opts;
3074
3075    let mut facts = Vec::new();
3076    let mut promotions = Vec::new();
3077    let mut hit_tier = None;
3078    let mut value = None;
3079
3080    for (index, tier) in tiers.iter().enumerate() {
3081        let info = lookup_tier(index as isize, &tier_names);
3082        let read = if tier.supports_versioned() {
3083            match tier.get_versioned(&key) {
3084                Ok(KvVersionedRead::Hit { value, generation }) => {
3085                    Ok((Some(value), Some(generation)))
3086                }
3087                Ok(KvVersionedRead::Miss { generation }) => Ok((None, Some(generation))),
3088                Err(error) => Err(error),
3089            }
3090        } else {
3091            tier.get(&key).map(|found| (found, None))
3092        };
3093
3094        match read {
3095            Ok((Some(found), generation)) => {
3096                facts.push(ReadThroughLookupFact {
3097                    outcome: ReadThroughOutcome::Hit,
3098                    key: key.clone(),
3099                    tier: info.clone(),
3100                    value: Some(found.clone()),
3101                    generation,
3102                    error: None,
3103                });
3104                hit_tier = Some(info);
3105                value = Some(found);
3106                break;
3107            }
3108            Ok((None, generation)) => {
3109                facts.push(ReadThroughLookupFact {
3110                    outcome: ReadThroughOutcome::Miss,
3111                    key: key.clone(),
3112                    tier: info.clone(),
3113                    value: None,
3114                    generation,
3115                    error: None,
3116                });
3117                call_on_miss(&on_miss, &key, info);
3118            }
3119            Err(error) => {
3120                facts.push(ReadThroughLookupFact {
3121                    outcome: ReadThroughOutcome::Error,
3122                    key: key.clone(),
3123                    tier: info.clone(),
3124                    value: None,
3125                    generation: None,
3126                    error: Some(error.clone()),
3127                });
3128                call_on_error(&on_error, &key, info, ReadThroughErrorStage::Lookup, error);
3129            }
3130        }
3131    }
3132
3133    if hit_tier.is_none() {
3134        if let Some(load) = load {
3135            let loader_tier = ReadThroughLookupTier {
3136                index: -1,
3137                name: Some("load".to_owned()),
3138            };
3139            match load(&key) {
3140                Ok(Some(loaded)) => {
3141                    facts.push(ReadThroughLookupFact {
3142                        outcome: ReadThroughOutcome::Hit,
3143                        key: key.clone(),
3144                        tier: loader_tier.clone(),
3145                        value: Some(loaded.clone()),
3146                        generation: None,
3147                        error: None,
3148                    });
3149                    hit_tier = Some(loader_tier);
3150                    value = Some(loaded);
3151                }
3152                Ok(None) => {
3153                    facts.push(ReadThroughLookupFact {
3154                        outcome: ReadThroughOutcome::Miss,
3155                        key: key.clone(),
3156                        tier: loader_tier.clone(),
3157                        value: None,
3158                        generation: None,
3159                        error: None,
3160                    });
3161                    call_on_miss(&on_miss, &key, loader_tier);
3162                }
3163                Err(error) => {
3164                    facts.push(ReadThroughLookupFact {
3165                        outcome: ReadThroughOutcome::Error,
3166                        key: key.clone(),
3167                        tier: loader_tier.clone(),
3168                        value: None,
3169                        generation: None,
3170                        error: Some(error.clone()),
3171                    });
3172                    call_on_error(
3173                        &on_error,
3174                        &key,
3175                        loader_tier,
3176                        ReadThroughErrorStage::Lookup,
3177                        error,
3178                    );
3179                }
3180            }
3181        }
3182    }
3183
3184    if let (Some(found), Some(source_tier)) = (value.as_ref(), hit_tier.as_ref()) {
3185        let source_index = if source_tier.index < 0 {
3186            tiers.len()
3187        } else {
3188            source_tier.index as usize
3189        };
3190        for index in build_promotion_targets(tiers.len(), source_index, &promote_to) {
3191            let tier = tiers[index];
3192            let info = lookup_tier(index as isize, &tier_names);
3193            let write = if tier.supports_versioned() {
3194                if let Some(generation) = generation_for_tier(&facts, index) {
3195                    tier.set_if_match(&key, found.clone(), generation)
3196                } else {
3197                    Err(StorageError::backend(
3198                        "tiered_read_through: versioned promotion target was not observed with a generation",
3199                    ))
3200                }
3201            } else {
3202                tier.set(&key, found.clone()).map(|()| true)
3203            };
3204
3205            match write {
3206                Ok(ok) => promotions.push(ReadThroughPromotionFact {
3207                    tier: info,
3208                    ok,
3209                    error: None,
3210                }),
3211                Err(error) => {
3212                    promotions.push(ReadThroughPromotionFact {
3213                        tier: info.clone(),
3214                        ok: false,
3215                        error: Some(error.clone()),
3216                    });
3217                    call_on_error(
3218                        &on_error,
3219                        &key,
3220                        info,
3221                        ReadThroughErrorStage::Promotion,
3222                        error,
3223                    );
3224                }
3225            }
3226        }
3227    }
3228
3229    let status = if hit_tier.is_some() && value.is_some() {
3230        TieredReadThroughStatus::Hit
3231    } else if facts
3232        .iter()
3233        .any(|fact| fact.outcome == ReadThroughOutcome::Error)
3234    {
3235        TieredReadThroughStatus::Error
3236    } else {
3237        TieredReadThroughStatus::Miss
3238    };
3239
3240    TieredReadThroughResult {
3241        status,
3242        key,
3243        value,
3244        hit_tier,
3245        facts,
3246        promotions,
3247    }
3248}
3249
3250fn lookup_tier(index: isize, tier_names: &[String]) -> ReadThroughLookupTier {
3251    ReadThroughLookupTier {
3252        index,
3253        name: tier_names.get(index.max(0) as usize).cloned(),
3254    }
3255}
3256
3257fn generation_for_tier<T>(
3258    facts: &[ReadThroughLookupFact<T>],
3259    index: usize,
3260) -> Option<&KvGeneration> {
3261    facts
3262        .iter()
3263        .find(|fact| fact.tier.index == index as isize)
3264        .and_then(|fact| fact.generation.as_ref())
3265}
3266
3267fn build_promotion_targets(
3268    tier_count: usize,
3269    hit_index: usize,
3270    promote_to: &PromotionPolicy,
3271) -> Vec<usize> {
3272    if tier_count == 0 {
3273        return Vec::new();
3274    }
3275    let max_promote = hit_index.min(tier_count);
3276    match promote_to {
3277        PromotionPolicy::Disabled => Vec::new(),
3278        PromotionPolicy::AllEarlier => (0..max_promote).collect(),
3279        PromotionPolicy::Indices(indices) => {
3280            let mut out = Vec::new();
3281            for &index in indices {
3282                if index < tier_count && index < max_promote && !out.contains(&index) {
3283                    out.push(index);
3284                }
3285            }
3286            out
3287        }
3288    }
3289}
3290
3291fn call_on_miss(
3292    on_miss: &Option<Box<ReadThroughMissFn<'_>>>,
3293    key: &str,
3294    tier: ReadThroughLookupTier,
3295) {
3296    if let Some(on_miss) = on_miss {
3297        let _ = catch_unwind(AssertUnwindSafe(|| {
3298            on_miss(ReadThroughMissContext {
3299                key: key.to_owned(),
3300                tier,
3301            });
3302        }));
3303    }
3304}
3305
3306fn call_on_error(
3307    on_error: &Option<Box<ReadThroughErrorFn<'_>>>,
3308    key: &str,
3309    tier: ReadThroughLookupTier,
3310    stage: ReadThroughErrorStage,
3311    error: StorageError,
3312) {
3313    if let Some(on_error) = on_error {
3314        let _ = catch_unwind(AssertUnwindSafe(|| {
3315            on_error(ReadThroughErrorContext {
3316                key: key.to_owned(),
3317                tier,
3318                stage,
3319                error,
3320            });
3321        }));
3322    }
3323}