1use 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
29pub type StorageResult<T> = Result<T, StorageError>;
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33pub enum StorageError {
35 Unsupported {
37 label: String,
39 capability: String,
41 },
42 ContentAddressedMiss {
44 key: String,
46 },
47 Backend(String),
49}
50
51impl StorageError {
52 pub fn backend(message: impl Into<String>) -> Self {
54 Self::Backend(message.into())
55 }
56
57 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
85pub trait ByteStorageBackend {
87 fn get(&self, key: &str) -> StorageResult<Option<Vec<u8>>>;
89 fn put(&self, key: &str, value: &[u8]) -> StorageResult<()>;
91 fn put_if_absent(&self, _key: &str, _value: &[u8]) -> StorageResult<bool> {
93 Err(StorageError::unsupported("byteStorage", "put-if-absent"))
94 }
95 fn delete(&self, key: &str) -> StorageResult<()>;
97 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)]
146pub struct FileBackendOptions {
148 pub namespace: String,
150 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 pub fn new() -> Self {
166 Self::default()
167 }
168
169 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
171 self.namespace = namespace.into();
172 self
173 }
174
175 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)]
183pub struct FileBackend {
185 dir: PathBuf,
186 namespace: String,
187 extension: String,
188}
189
190pub 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 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#[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)]
468pub enum KvVersionedRead<T> {
470 Hit {
472 value: T,
474 generation: KvGeneration,
476 },
477 Miss {
479 generation: KvGeneration,
481 },
482}
483
484impl<T> KvVersionedRead<T> {
485 pub fn generation(&self) -> &KvGeneration {
487 match self {
488 Self::Hit { generation, .. } | Self::Miss { generation } => generation,
489 }
490 }
491}
492
493pub trait KvStorageTier<T: Clone> {
495 fn get(&self, key: &str) -> StorageResult<Option<T>>;
497 fn set(&self, key: &str, value: T) -> StorageResult<()>;
499 fn put_if_absent(&self, _key: &str, _value: T) -> StorageResult<bool> {
501 Err(StorageError::unsupported("kvStorage", "put-if-absent"))
502 }
503 fn delete(&self, key: &str) -> StorageResult<()>;
505 fn list(&self, prefix: &str) -> StorageResult<Vec<String>>;
507
508 fn supports_versioned(&self) -> bool {
510 false
511 }
512
513 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 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#[derive(Clone, Debug)]
552pub struct MemoryKv<T: Clone> {
553 inner: Rc<MemoryKvInner<T>>,
554}
555
556pub 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
569pub 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 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)]
704pub struct CodecKvStorage<B, C, T> {
706 backend: B,
707 codec: C,
708 marker: std::marker::PhantomData<T>,
709}
710
711pub 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
757pub type FileKv<T, C> = CodecKvStorage<FileBackend, C, T>;
759
760pub 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)]
774pub struct FileAppendLogOptions {
776 pub backend: FileBackendOptions,
778 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 pub fn new() -> Self {
794 Self::default()
795 }
796
797 pub fn with_backend(mut self, backend: FileBackendOptions) -> Self {
799 self.backend = backend;
800 self
801 }
802
803 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
805 self.prefix = prefix.into();
806 self
807 }
808}
809
810pub 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)]
825pub enum ContentAddressedMode {
827 Read,
829 Write,
831 #[default]
832 ReadWrite,
834 ReadStrict,
836}
837
838pub type ContentAddressedKeyContext<Ctx> = dyn Fn(&Ctx) -> Result<Value, JsonCodecError>;
840
841pub struct ContentAddressedKvOptions<Ctx, V: Clone> {
843 pub kv: Rc<dyn KvStorageTier<V>>,
845 pub key_context: Rc<ContentAddressedKeyContext<Ctx>>,
847 pub key_prefix: Option<String>,
849 pub mode: ContentAddressedMode,
851}
852
853impl<V: Clone> ContentAddressedKvOptions<Value, V> {
854 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 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 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 pub fn with_key_prefix(mut self, prefix: impl Into<String>) -> Self {
890 self.key_prefix = Some(prefix.into());
891 self
892 }
893
894 pub fn with_mode(mut self, mode: ContentAddressedMode) -> Self {
896 self.mode = mode;
897 self
898 }
899}
900
901#[derive(Clone)]
902pub 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
919pub type ContentAddressedStorage<Ctx, V> = ContentAddressedKv<Ctx, V>;
921pub type ContentAddressedStorageOptions<Ctx, V> = ContentAddressedKvOptions<Ctx, V>;
923
924pub 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
936pub 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 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 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 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 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")]
1007pub enum ChangeLifecycle {
1009 Spec,
1011 Data,
1013 Ownership,
1015}
1016
1017pub const WAL_KEY_SEGMENT: &str = "wal";
1019pub const WAL_FRAME_SEQ_PAD: usize = 20;
1021pub const WAL_FORMAT_VERSION: u64 = 1;
1023
1024pub type WalFrameTimestampNs = String;
1026
1027#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1028pub struct WalFrameBody<T> {
1030 pub t: String,
1032 pub lifecycle: ChangeLifecycle,
1034 pub path: String,
1036 pub change: T,
1038 pub frame_seq: u64,
1040 pub frame_t_ns: WalFrameTimestampNs,
1042 pub format_version: u64,
1044}
1045
1046#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1047pub struct WalFrame<T> {
1049 pub t: String,
1051 pub lifecycle: ChangeLifecycle,
1053 pub path: String,
1055 pub change: T,
1057 pub frame_seq: u64,
1059 pub frame_t_ns: WalFrameTimestampNs,
1061 pub format_version: u64,
1063 pub checksum: String,
1065}
1066
1067#[derive(Clone, Debug, PartialEq)]
1068pub struct WalFrameOptions<T> {
1070 pub path: String,
1072 pub change: T,
1074 pub frame_seq: u64,
1076 pub lifecycle: ChangeLifecycle,
1078 pub frame_t_ns: Option<String>,
1080}
1081
1082impl<T> WalFrameOptions<T> {
1083 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 pub fn with_lifecycle(mut self, lifecycle: ChangeLifecycle) -> Self {
1096 self.lifecycle = lifecycle;
1097 self
1098 }
1099
1100 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
1107pub 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
1116pub fn wal_frame_key(prefix: &str, frame_seq: u64) -> String {
1118 format!("{prefix}/{frame_seq:0>width$}", width = WAL_FRAME_SEQ_PAD)
1119}
1120
1121pub 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
1130pub 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
1161pub 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
1180pub 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)]
1196pub struct WalFrameCodec<T> {
1198 marker: std::marker::PhantomData<T>,
1199}
1200
1201pub 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)]
1347pub struct ChangeEnvelope<T> {
1349 pub lifecycle: ChangeLifecycle,
1351 pub structure: String,
1353 pub version: Value,
1355 pub t_ns: String,
1357 #[serde(skip_serializing_if = "Option::is_none")]
1358 pub seq: Option<u64>,
1360 pub change: T,
1362}
1363
1364#[derive(Clone, Debug)]
1365pub struct ChangeEnvelopeOptions {
1367 pub lifecycle: ChangeLifecycle,
1369 pub structure: String,
1371 pub version: Value,
1373 pub t_ns: Option<String>,
1375 pub seq: Option<u64>,
1377}
1378
1379impl ChangeEnvelopeOptions {
1380 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 pub fn with_lifecycle(mut self, lifecycle: ChangeLifecycle) -> Self {
1393 self.lifecycle = lifecycle;
1394 self
1395 }
1396
1397 pub fn with_version(mut self, version: impl Into<Value>) -> Self {
1399 self.version = version.into();
1400 self
1401 }
1402
1403 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 pub fn with_seq(mut self, seq: u64) -> Self {
1411 self.seq = Some(seq);
1412 self
1413 }
1414}
1415
1416pub 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
1424pub 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)]
1455pub struct ChangeEnvelopeCodec<T> {
1457 marker: std::marker::PhantomData<T>,
1458}
1459
1460pub 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
1485pub 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)]
1554pub struct ObserveEventFrame<T> {
1556 pub lifecycle: ChangeLifecycle,
1558 pub structure: String,
1560 pub version: Value,
1562 pub t_ns: String,
1564 #[serde(skip_serializing_if = "Option::is_none")]
1565 pub seq: Option<u64>,
1567 pub change: T,
1569 #[serde(rename = "observeSeq")]
1570 pub observe_seq: u64,
1572 pub path: String,
1574 #[serde(skip_serializing_if = "Option::is_none")]
1575 pub stream: Option<String>,
1577}
1578
1579#[derive(Clone, Debug, Default, Eq, PartialEq)]
1580pub struct ObserveEventFrameOptions {
1582 pub stream: Option<String>,
1584}
1585
1586impl ObserveEventFrameOptions {
1587 pub fn with_stream(mut self, stream: impl Into<String>) -> Self {
1589 self.stream = Some(stream.into());
1590 self
1591 }
1592}
1593
1594pub 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
1618pub 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)]
1639pub struct ObserveEventFrameCodec<T> {
1641 marker: std::marker::PhantomData<T>,
1642}
1643
1644pub 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
1702pub type ObserveEventLogPage<T> = AppendLogPage<ObserveEventFrame<T>>;
1704
1705pub 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
1713pub const APPEND_LOG_SEQ_PAD: usize = 20;
1715
1716#[derive(Clone, Debug, PartialEq)]
1717pub struct AppendLogEntry<T> {
1719 pub key: String,
1721 pub seq: u64,
1723 pub value: T,
1725}
1726
1727#[derive(Clone, Debug, Default, Eq, PartialEq)]
1728pub struct AppendLogReadOptions {
1730 pub after: Option<u64>,
1732 pub limit: Option<usize>,
1734}
1735
1736#[derive(Clone, Debug, PartialEq)]
1737pub struct AppendLogPage<T> {
1739 pub entries: Vec<AppendLogEntry<T>>,
1741 pub next_after: Option<u64>,
1743 pub done: bool,
1745}
1746
1747pub trait AppendLogStorageTier<T: Clone> {
1749 fn append(&self, value: T) -> StorageResult<AppendLogEntry<T>>;
1751 fn read(&self, opts: AppendLogReadOptions) -> StorageResult<Vec<AppendLogEntry<T>>>;
1753 fn truncate_after(&self, seq: u64) -> StorageResult<()>;
1755 fn size(&self) -> StorageResult<usize>;
1757}
1758
1759#[derive(Clone)]
1760pub 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)]
1775pub 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
1791pub fn append_log_key(prefix: &str, seq: u64) -> String {
1793 format!("{prefix}/{seq:0APPEND_LOG_SEQ_PAD$}")
1794}
1795
1796pub 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
1807pub fn memory_append_log<T: Clone + 'static>(prefix: impl Into<String>) -> AppendLogStorage<T> {
1809 append_log_storage(Rc::new(memory_kv()), prefix)
1810}
1811
1812pub 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
1830pub 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
1838pub 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
1869pub const REACTIVE_COLLECTION_SNAPSHOT_FORMAT: &str = "graphrefly.reactive-collection.snapshot.v1";
1871pub const REACTIVE_COLLECTION_CHANGE_FORMAT: &str = "graphrefly.reactive-collection.change.v1";
1873pub const REACTIVE_COLLECTION_FRAME_VERSION: u8 = 1;
1875
1876#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1877pub enum ReactiveCollectionKind {
1879 #[serde(rename = "reactiveList")]
1880 ReactiveList,
1882 #[serde(rename = "reactiveLog")]
1883 ReactiveLog,
1885 #[serde(rename = "reactiveMap")]
1886 ReactiveMap,
1888 #[serde(rename = "reactiveIndex")]
1889 ReactiveIndex,
1891}
1892
1893#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1894#[serde(deny_unknown_fields)]
1895pub struct ReactiveCollectionSnapshotFrame {
1897 pub format: String,
1899 pub version: u8,
1901 pub kind: ReactiveCollectionKind,
1903 #[serde(rename = "changeCursor")]
1904 pub change_cursor: i64,
1906 pub snapshot: Value,
1908}
1909
1910#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1911#[serde(deny_unknown_fields)]
1912pub struct ReactiveCollectionChangeFrame {
1914 pub format: String,
1916 pub version: u8,
1918 pub kind: ReactiveCollectionKind,
1920 pub change: Value,
1922}
1923
1924#[derive(Clone, Debug, PartialEq)]
1925pub struct ReactiveCollectionRestoreState<T> {
1927 pub kind: ReactiveCollectionKind,
1929 pub state: T,
1931 pub source: ReactiveCollectionRestoreSource,
1933 pub snapshot: ReactiveCollectionSnapshotRestoreMeta,
1935 pub changes: ReactiveCollectionChangesRestoreMeta,
1937 pub cursor: Option<u64>,
1939 pub snapshot_found: bool,
1941 pub changes_applied: usize,
1943}
1944
1945pub type ReactiveListRestoreState<T> = ReactiveCollectionRestoreState<Vec<T>>;
1947pub type ReactiveLogRestoreState<T> = ReactiveCollectionRestoreState<Vec<T>>;
1949pub type ReactiveMapRestoreState<K, V> = ReactiveCollectionRestoreState<Vec<(K, V)>>;
1951pub 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)]
1962pub enum ReactiveCollectionRestoreSource {
1964 Empty,
1966 Changes,
1968 Snapshot,
1970 SnapshotAndChanges,
1972}
1973
1974#[derive(Clone, Debug, Eq, PartialEq)]
1975pub struct ReactiveCollectionSnapshotRestoreMeta {
1977 pub found: bool,
1979 pub change_cursor: i64,
1981}
1982
1983#[derive(Clone, Debug, Eq, PartialEq)]
1984pub struct ReactiveCollectionChangesRestoreMeta {
1986 pub applied: usize,
1988 pub cursor: i64,
1990}
1991
1992#[derive(Clone, Default)]
1993pub struct LoadReactiveCollectionStateOptions<'a> {
1995 pub storage_prefix: Option<&'a str>,
1997 pub snapshot_key: Option<&'a str>,
1999 pub change_log: Option<&'a dyn AppendLogStorageTier<ReactiveCollectionChangeFrame>>,
2001}
2002
2003#[derive(Clone, Copy, Debug, Default)]
2004pub struct ReactiveCollectionSnapshotFrameCodec;
2006
2007#[derive(Clone, Copy, Debug, Default)]
2008pub struct ReactiveCollectionChangeFrameCodec;
2010
2011pub 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
2021pub 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
2038pub 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
2053pub fn reactive_collection_snapshot_frame_codec() -> ReactiveCollectionSnapshotFrameCodec {
2055 ReactiveCollectionSnapshotFrameCodec
2056}
2057
2058pub fn reactive_collection_change_frame_codec() -> ReactiveCollectionChangeFrameCodec {
2060 ReactiveCollectionChangeFrameCodec
2061}
2062
2063pub 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
2089pub 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
2153pub 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
2185pub 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
2217pub 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
2252pub 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)]
2898pub struct ReadThroughLookupTier {
2900 pub index: isize,
2902 pub name: Option<String>,
2904}
2905
2906#[derive(Clone, Debug, Eq, PartialEq)]
2907pub enum ReadThroughOutcome {
2909 Hit,
2911 Miss,
2913 Error,
2915}
2916
2917#[derive(Clone, Debug)]
2918pub struct ReadThroughLookupFact<T> {
2920 pub outcome: ReadThroughOutcome,
2922 pub key: String,
2924 pub tier: ReadThroughLookupTier,
2926 pub value: Option<T>,
2928 pub generation: Option<KvGeneration>,
2930 pub error: Option<StorageError>,
2932}
2933
2934#[derive(Clone, Debug, PartialEq)]
2935pub struct ReadThroughPromotionFact {
2937 pub tier: ReadThroughLookupTier,
2939 pub ok: bool,
2941 pub error: Option<StorageError>,
2943}
2944
2945#[derive(Clone, Debug, Eq, PartialEq)]
2946pub enum TieredReadThroughStatus {
2948 Hit,
2950 Miss,
2952 Error,
2954}
2955
2956#[derive(Clone, Debug)]
2957pub struct TieredReadThroughResult<T> {
2959 pub status: TieredReadThroughStatus,
2961 pub key: String,
2963 pub value: Option<T>,
2965 pub hit_tier: Option<ReadThroughLookupTier>,
2967 pub facts: Vec<ReadThroughLookupFact<T>>,
2969 pub promotions: Vec<ReadThroughPromotionFact>,
2971}
2972
2973#[derive(Clone, Debug, Default, Eq, PartialEq)]
2974pub enum PromotionPolicy {
2976 #[default]
2977 AllEarlier,
2979 Disabled,
2981 Indices(Vec<usize>),
2983}
2984
2985pub struct ReadThroughMissContext {
2987 pub key: String,
2989 pub tier: ReadThroughLookupTier,
2991}
2992
2993pub struct ReadThroughErrorContext {
2995 pub key: String,
2997 pub tier: ReadThroughLookupTier,
2999 pub stage: ReadThroughErrorStage,
3001 pub error: StorageError,
3003}
3004
3005#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3006pub enum ReadThroughErrorStage {
3008 Lookup,
3010 Promotion,
3012}
3013
3014pub type ReadThroughLoadFn<'a, T> = dyn Fn(&str) -> StorageResult<Option<T>> + 'a;
3016pub type ReadThroughMissFn<'a> = dyn Fn(ReadThroughMissContext) + 'a;
3018pub type ReadThroughErrorFn<'a> = dyn Fn(ReadThroughErrorContext) + 'a;
3020
3021pub struct TieredReadThroughOptions<'a, T: Clone> {
3023 pub key: String,
3025 pub tiers: Vec<&'a dyn KvStorageTier<T>>,
3027 pub tier_names: Vec<String>,
3029 pub load: Option<Box<ReadThroughLoadFn<'a, T>>>,
3031 pub promote_to: PromotionPolicy,
3033 pub on_miss: Option<Box<ReadThroughMissFn<'a>>>,
3035 pub on_error: Option<Box<ReadThroughErrorFn<'a>>>,
3037}
3038
3039impl<'a, T: Clone> TieredReadThroughOptions<'a, T> {
3040 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
3054pub fn read_through_kv<T: Clone>(
3056 opts: TieredReadThroughOptions<'_, T>,
3057) -> TieredReadThroughResult<T> {
3058 tiered_read_through(opts)
3059}
3060
3061pub 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}