1use std::collections::{HashMap, VecDeque};
7use std::marker::PhantomData;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::time::SystemTime;
11
12use dashmap::DashMap;
13
14use crate::clock::{SharedClock, system_clock};
15use serde_json::Value;
16
17const DEFAULT_MUTATION_JOURNAL_CAPACITY: usize = 1024;
18
19pub struct StateKey<T> {
31 key: &'static str,
32 _phantom: PhantomData<fn() -> T>,
33}
34
35impl<T> StateKey<T> {
36 pub const fn new(key: &'static str) -> Self {
38 Self {
39 key,
40 _phantom: PhantomData,
41 }
42 }
43
44 pub const fn key(&self) -> &'static str {
46 self.key
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(rename_all = "snake_case")]
53pub enum StateMutationOrigin {
54 Set,
56 SetCommitted,
58 Remove,
60 ClearPrefix,
62 Commit,
64}
65
66#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
71pub struct StateMutation {
72 pub sequence: u64,
74 pub key: String,
76 pub old: Option<Value>,
78 pub new: Option<Value>,
80 pub origin: StateMutationOrigin,
82 #[serde(rename = "timestamp_ms", with = "systemtime_epoch_millis")]
85 pub timestamp: SystemTime,
86 pub delta: bool,
88}
89
90mod systemtime_epoch_millis {
92 use std::time::{Duration, SystemTime, UNIX_EPOCH};
93
94 use serde::{Deserialize, Deserializer, Serializer};
95
96 pub(super) fn serialize<S: Serializer>(t: &SystemTime, ser: S) -> Result<S::Ok, S::Error> {
97 let millis = t
98 .duration_since(UNIX_EPOCH)
99 .map(|d| d.as_millis() as u64)
100 .unwrap_or(0);
101 ser.serialize_u64(millis)
102 }
103
104 pub(super) fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<SystemTime, D::Error> {
105 let millis = u64::deserialize(de)?;
106 Ok(UNIX_EPOCH + Duration::from_millis(millis))
107 }
108}
109
110pub trait JournalSink: Send + Sync {
120 fn write(&self, m: &StateMutation);
122}
123
124#[derive(Clone, Default)]
127struct JournalSinkSlot(Arc<parking_lot::RwLock<Option<Arc<dyn JournalSink>>>>);
128
129pub const REDACTED: &str = "[redacted]";
132
133#[derive(Clone, Default, Debug)]
135struct RedactionSlot(Arc<parking_lot::RwLock<Arc<std::collections::BTreeSet<String>>>>);
136
137#[derive(Clone)]
139struct ClockSlot(Arc<parking_lot::RwLock<SharedClock>>);
140
141impl Default for ClockSlot {
142 fn default() -> Self {
143 Self(Arc::new(parking_lot::RwLock::new(system_clock())))
144 }
145}
146
147impl std::fmt::Debug for ClockSlot {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_tuple("ClockSlot").field(&*self.0.read()).finish()
150 }
151}
152
153impl std::fmt::Debug for JournalSinkSlot {
154 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155 let installed = self.0.read().is_some();
156 f.debug_tuple("JournalSinkSlot").field(&installed).finish()
157 }
158}
159
160const JOURNAL_FLUSH_INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
161
162fn journal_log_error(context: &'static str, e: &dyn std::fmt::Display) {
165 tracing::warn!(error = %e, "{context}");
166}
167
168struct FileJournalInner {
169 writer: std::io::BufWriter<std::fs::File>,
170 last_flush: std::time::Instant,
171}
172
173pub struct FileJournalSink {
183 inner: parking_lot::Mutex<FileJournalInner>,
184}
185
186impl FileJournalSink {
187 pub fn create(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
189 let file = std::fs::File::create(path)?;
190 Ok(Self {
191 inner: parking_lot::Mutex::new(FileJournalInner {
192 writer: std::io::BufWriter::new(file),
193 last_flush: std::time::Instant::now(),
194 }),
195 })
196 }
197
198 pub fn flush(&self) {
200 let mut inner = self.inner.lock();
201 if let Err(e) = std::io::Write::flush(&mut inner.writer) {
202 journal_log_error("FileJournalSink flush failed", &e);
203 }
204 inner.last_flush = std::time::Instant::now();
205 }
206}
207
208impl JournalSink for FileJournalSink {
209 fn write(&self, m: &StateMutation) {
210 let line = match serde_json::to_string(m) {
211 Ok(line) => line,
212 Err(e) => {
213 journal_log_error("FileJournalSink serialize failed", &e);
214 return;
215 }
216 };
217 let mut inner = self.inner.lock();
218 if let Err(e) = std::io::Write::write_all(&mut inner.writer, line.as_bytes())
219 .and_then(|()| std::io::Write::write_all(&mut inner.writer, b"\n"))
220 {
221 journal_log_error("FileJournalSink write failed", &e);
222 return;
223 }
224 if inner.last_flush.elapsed() >= JOURNAL_FLUSH_INTERVAL {
225 if let Err(e) = std::io::Write::flush(&mut inner.writer) {
226 journal_log_error("FileJournalSink flush failed", &e);
227 }
228 inner.last_flush = std::time::Instant::now();
229 }
230 }
231}
232
233pub fn read_journal(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<StateMutation>> {
235 parse_journal(&std::fs::read_to_string(path)?)
236}
237
238pub fn parse_journal(data: &str) -> std::io::Result<Vec<StateMutation>> {
240 data.lines()
241 .enumerate()
242 .filter(|(_, line)| !line.trim().is_empty())
243 .map(|(n, line)| {
244 serde_json::from_str(line).map_err(|e| {
245 std::io::Error::new(
246 std::io::ErrorKind::InvalidData,
247 format!("journal line {}: {e}", n + 1),
248 )
249 })
250 })
251 .collect()
252}
253
254impl Drop for FileJournalSink {
255 fn drop(&mut self) {
256 if let Err(e) = std::io::Write::flush(&mut self.inner.lock().writer) {
257 journal_log_error("FileJournalSink final flush failed", &e);
258 }
259 }
260}
261
262#[derive(Default)]
264pub struct MemoryJournalSink {
265 entries: parking_lot::Mutex<Vec<StateMutation>>,
266}
267
268impl MemoryJournalSink {
269 pub fn new() -> Self {
271 Self::default()
272 }
273
274 pub fn entries(&self) -> Vec<StateMutation> {
276 self.entries.lock().clone()
277 }
278
279 pub fn len(&self) -> usize {
281 self.entries.lock().len()
282 }
283
284 pub fn is_empty(&self) -> bool {
286 self.entries.lock().is_empty()
287 }
288}
289
290impl JournalSink for MemoryJournalSink {
291 fn write(&self, m: &StateMutation) {
292 self.entries.lock().push(m.clone());
293 }
294}
295
296#[derive(Debug, thiserror::Error)]
298pub enum StateError {
299 #[error("failed to serialize state value for key '{key}': {source}")]
301 Serialize {
302 key: String,
304 source: serde_json::Error,
306 },
307 #[error("state value at key '{key}' is not the requested type: {source}")]
310 WrongType {
311 key: String,
313 source: serde_json::Error,
315 },
316}
317
318#[derive(Debug, Clone)]
324enum DeltaOp {
325 Put(Value),
327 Delete,
329}
330
331#[derive(Debug, Clone, serde::Serialize)]
338pub struct SlotEvidence {
339 pub key: String,
341 pub present: bool,
343 pub value: Option<Value>,
345 pub source: Option<String>,
348 pub confidence: Option<f64>,
350 pub last_sequence: Option<u64>,
353 pub last_origin: Option<StateMutationOrigin>,
355}
356
357#[derive(Debug, Clone)]
363pub struct State {
364 inner: Arc<DashMap<String, Value>>,
365 delta: Arc<DashMap<String, DeltaOp>>,
366 mutations: Arc<std::sync::Mutex<VecDeque<StateMutation>>>,
367 next_mutation_sequence: Arc<AtomicU64>,
368 mutation_capacity: usize,
369 journal_sink: JournalSinkSlot,
370 clock: ClockSlot,
371 redacted: RedactionSlot,
372 track_delta: bool,
373}
374
375impl Default for State {
376 fn default() -> Self {
377 Self::new()
378 }
379}
380
381impl State {
382 pub fn new() -> Self {
384 Self {
385 inner: Arc::new(DashMap::new()),
386 delta: Arc::new(DashMap::new()),
387 mutations: Arc::new(std::sync::Mutex::new(VecDeque::new())),
388 next_mutation_sequence: Arc::new(AtomicU64::new(1)),
389 mutation_capacity: DEFAULT_MUTATION_JOURNAL_CAPACITY,
390 journal_sink: JournalSinkSlot::default(),
391 clock: ClockSlot::default(),
392 redacted: RedactionSlot::default(),
393 track_delta: false,
394 }
395 }
396
397 pub fn with_delta_tracking(&self) -> State {
400 State {
401 inner: self.inner.clone(),
402 delta: Arc::new(DashMap::new()),
403 mutations: self.mutations.clone(),
404 next_mutation_sequence: self.next_mutation_sequence.clone(),
405 mutation_capacity: self.mutation_capacity,
406 journal_sink: self.journal_sink.clone(),
407 clock: self.clock.clone(),
408 redacted: self.redacted.clone(),
409 track_delta: true,
410 }
411 }
412
413 pub fn set_journal_sink(&self, sink: Arc<dyn JournalSink>) {
421 *self.journal_sink.0.write() = Some(sink);
422 }
423
424 pub fn with_journal_sink(self, sink: Arc<dyn JournalSink>) -> Self {
426 self.set_journal_sink(sink);
427 self
428 }
429
430 pub fn set_clock(&self, clock: SharedClock) {
437 *self.clock.0.write() = clock;
438 }
439
440 pub fn with_clock(self, clock: SharedClock) -> Self {
442 self.set_clock(clock);
443 self
444 }
445
446 pub fn clock(&self) -> SharedClock {
448 self.clock.0.read().clone()
449 }
450
451 pub fn redact_keys<I, S>(&self, keys: I)
462 where
463 I: IntoIterator<Item = S>,
464 S: Into<String>,
465 {
466 let mut guard = self.redacted.0.write();
467 let mut set = (**guard).clone();
468 set.extend(keys.into_iter().map(Into::into));
469 *guard = Arc::new(set);
470 }
471
472 pub fn redacted_keys(&self) -> std::collections::BTreeSet<String> {
474 (**self.redacted.0.read()).clone()
475 }
476
477 pub fn is_redacted(&self, key: &str) -> bool {
480 let set = self.redacted.0.read().clone();
481 if set.is_empty() {
482 return false;
483 }
484 set.contains(key)
485 || key
486 .rsplit_once(':')
487 .is_some_and(|(_, base)| set.contains(base))
488 }
489
490 pub fn redact_value(&self, key: &str, value: &Value) -> Value {
492 if self.is_redacted(key) {
493 Value::String(REDACTED.into())
494 } else {
495 value.clone()
496 }
497 }
498
499 pub fn redact_fields(&self, value: &Value) -> Value {
502 match value {
503 Value::Object(obj) if !self.redacted.0.read().is_empty() => Value::Object(
504 obj.iter()
505 .map(|(k, v)| (k.clone(), self.redact_value(k, v)))
506 .collect(),
507 ),
508 other => other.clone(),
509 }
510 }
511
512 pub fn to_redacted_hashmap(&self) -> HashMap<String, Value> {
515 self.to_hashmap()
516 .into_iter()
517 .map(|(k, v)| {
518 let v = self.redact_value(&k, &v);
519 (k, v)
520 })
521 .collect()
522 }
523
524 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
531 self.get_raw(key)
532 .and_then(|v| serde_json::from_value(v).ok())
533 }
534
535 pub fn try_get<T: serde::de::DeserializeOwned>(
545 &self,
546 key: &str,
547 ) -> Result<Option<T>, StateError> {
548 match self.get_raw(key) {
549 None => Ok(None),
550 Some(v) => {
551 serde_json::from_value(v)
552 .map(Some)
553 .map_err(|source| StateError::WrongType {
554 key: key.to_string(),
555 source,
556 })
557 }
558 }
559 }
560
561 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
569 where
570 F: FnOnce(&Value) -> R,
571 {
572 if self.track_delta {
573 match self.delta.get(key).map(|r| r.value().clone()) {
574 Some(DeltaOp::Put(v)) => return Some(f(&v)),
575 Some(DeltaOp::Delete) => return None, None => {}
577 }
578 }
579 if let Some(ref_multi) = self.inner.get(key) {
580 return Some(f(ref_multi.value()));
581 }
582 if !key.contains(':') {
583 let mut derived_key = String::with_capacity(8 + key.len());
584 use std::fmt::Write;
585 let _ = write!(derived_key, "derived:{key}");
586 if self.track_delta {
587 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
588 Some(DeltaOp::Put(v)) => return Some(f(&v)),
589 Some(DeltaOp::Delete) => return None,
590 None => {}
591 }
592 }
593 if let Some(ref_multi) = self.inner.get(&derived_key) {
594 return Some(f(ref_multi.value()));
595 }
596 }
597 None
598 }
599
600 pub fn get_raw(&self, key: &str) -> Option<Value> {
605 if self.track_delta {
606 match self.delta.get(key).map(|r| r.value().clone()) {
607 Some(DeltaOp::Put(v)) => return Some(v),
608 Some(DeltaOp::Delete) => return None, None => {}
610 }
611 }
612 if let Some(v) = self.inner.get(key) {
613 return Some(v.value().clone());
614 }
615 if !key.contains(':') {
617 use std::fmt::Write;
618 let mut derived_key = String::with_capacity(8 + key.len());
619 let _ = write!(derived_key, "derived:{key}");
620 if self.track_delta {
621 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
622 Some(DeltaOp::Put(v)) => return Some(v),
623 Some(DeltaOp::Delete) => return None,
624 None => {}
625 }
626 }
627 return self.inner.get(&derived_key).map(|v| v.value().clone());
628 }
629 None
630 }
631
632 pub fn get_key<T: serde::de::DeserializeOwned>(&self, key: &StateKey<T>) -> Option<T> {
635 self.get(key.key())
636 }
637
638 pub fn try_get_key<T: serde::de::DeserializeOwned>(
641 &self,
642 key: &StateKey<T>,
643 ) -> Result<Option<T>, StateError> {
644 self.try_get(key.key())
645 }
646
647 pub fn set_key<T: serde::Serialize>(
651 &self,
652 key: &StateKey<T>,
653 value: T,
654 ) -> Result<(), StateError> {
655 self.set(key.key(), value)
656 }
657
658 pub fn with_key<T, F, R>(&self, key: &StateKey<T>, f: F) -> Option<R>
660 where
661 F: FnOnce(&Value) -> R,
662 {
663 self.with(key.key(), f)
664 }
665
666 pub fn set(
672 &self,
673 key: impl Into<String>,
674 value: impl serde::Serialize,
675 ) -> Result<(), StateError> {
676 let key = key.into();
677 let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
678 key: key.clone(),
679 source,
680 })?;
681 self.put_value(key, v, StateMutationOrigin::Set);
682 Ok(())
683 }
684
685 fn put_value(&self, key: String, v: Value, origin: StateMutationOrigin) {
690 let old = self.get_raw(&key);
691 if self.track_delta {
692 self.delta.insert(key.clone(), DeltaOp::Put(v.clone()));
693 } else {
694 self.inner.insert(key.clone(), v.clone());
695 }
696 self.record_mutation(key, old, Some(v), origin);
697 }
698
699 pub fn set_committed(
703 &self,
704 key: impl Into<String>,
705 value: impl serde::Serialize,
706 ) -> Result<(), StateError> {
707 let key = key.into();
708 let v = serde_json::to_value(value).map_err(|source| StateError::Serialize {
709 key: key.clone(),
710 source,
711 })?;
712 let old = self.inner.insert(key.clone(), v.clone());
713 self.record_mutation(key, old, Some(v), StateMutationOrigin::SetCommitted);
714 Ok(())
715 }
716
717 pub fn modify<T, F>(&self, key: &str, default: T, f: F) -> Result<T, StateError>
725 where
726 T: serde::Serialize + serde::de::DeserializeOwned,
727 F: FnOnce(T) -> T,
728 {
729 use dashmap::mapref::entry::Entry;
730
731 let serialize = |key: &str, val: &T| {
732 serde_json::to_value(val).map_err(|source| StateError::Serialize {
733 key: key.to_string(),
734 source,
735 })
736 };
737
738 if self.track_delta {
739 match self.delta.entry(key.to_string()) {
742 Entry::Occupied(mut o) => {
743 let current = match o.get() {
744 DeltaOp::Put(v) => serde_json::from_value(v.clone()).unwrap_or(default),
745 DeltaOp::Delete => default,
746 };
747 let old = self.inner.get(key).map(|r| r.value().clone());
748 let new_val = f(current);
749 let v = serialize(key, &new_val)?;
750 o.insert(DeltaOp::Put(v.clone()));
751 self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
752 Ok(new_val)
753 }
754 Entry::Vacant(slot) => {
755 let base = self
756 .inner
757 .get(key)
758 .and_then(|r| serde_json::from_value(r.value().clone()).ok());
759 let old = self.inner.get(key).map(|r| r.value().clone());
760 let new_val = f(base.unwrap_or(default));
761 let v = serialize(key, &new_val)?;
762 slot.insert(DeltaOp::Put(v.clone()));
763 self.record_mutation(key.to_string(), old, Some(v), StateMutationOrigin::Set);
764 Ok(new_val)
765 }
766 }
767 } else {
768 match self.inner.entry(key.to_string()) {
769 Entry::Occupied(mut o) => {
770 let old = o.get().clone();
771 let current = serde_json::from_value(old.clone()).unwrap_or(default);
772 let new_val = f(current);
773 let v = serialize(key, &new_val)?;
774 o.insert(v.clone());
775 self.record_mutation(
776 key.to_string(),
777 Some(old),
778 Some(v),
779 StateMutationOrigin::Set,
780 );
781 Ok(new_val)
782 }
783 Entry::Vacant(slot) => {
784 let new_val = f(default);
785 let v = serialize(key, &new_val)?;
786 slot.insert(v.clone());
787 self.record_mutation(key.to_string(), None, Some(v), StateMutationOrigin::Set);
788 Ok(new_val)
789 }
790 }
791 }
792 }
793
794 pub fn contains(&self, key: &str) -> bool {
802 if self.track_delta {
803 match self.delta.get(key).map(|r| r.value().clone()) {
804 Some(DeltaOp::Put(_)) => return true,
805 Some(DeltaOp::Delete) => return false, None => {}
807 }
808 }
809 if self.inner.contains_key(key) {
810 return true;
811 }
812 if !key.contains(':') {
813 let derived_key = format!("derived:{key}");
814 if self.track_delta {
815 match self.delta.get(&derived_key).map(|r| r.value().clone()) {
816 Some(DeltaOp::Put(_)) => return true,
817 Some(DeltaOp::Delete) => return false,
818 None => {}
819 }
820 }
821 return self.inner.contains_key(&derived_key);
822 }
823 false
824 }
825
826 pub fn remove(&self, key: &str) -> Option<Value> {
832 if self.track_delta {
833 let removed = self.get_raw(key);
834 self.delta.insert(key.to_string(), DeltaOp::Delete);
837 if let Some(ref old) = removed {
838 self.record_mutation(
839 key.to_string(),
840 Some(old.clone()),
841 None,
842 StateMutationOrigin::Remove,
843 );
844 }
845 removed
846 } else {
847 let removed = self.inner.remove(key).map(|(_, v)| v);
848 if let Some(ref old) = removed {
849 self.record_mutation(
850 key.to_string(),
851 Some(old.clone()),
852 None,
853 StateMutationOrigin::Remove,
854 );
855 }
856 removed
857 }
858 }
859
860 pub fn keys(&self) -> Vec<String> {
864 if !self.track_delta || self.delta.is_empty() {
865 return self.inner.iter().map(|r| r.key().clone()).collect();
866 }
867 let mut seen =
868 std::collections::HashSet::with_capacity(self.inner.len() + self.delta.len());
869 let mut keys = Vec::with_capacity(self.inner.len() + self.delta.len());
870 for entry in self.delta.iter() {
872 let key = entry.key().clone();
873 seen.insert(key.clone());
874 if matches!(entry.value(), DeltaOp::Put(_)) {
875 keys.push(key);
876 }
877 }
878 for entry in self.inner.iter() {
879 let key = entry.key().clone();
880 if seen.insert(key.clone()) {
881 keys.push(key);
882 }
883 }
884 keys
885 }
886
887 pub fn pick(&self, keys: &[&str]) -> State {
889 let new = State::new().with_clock(self.clock());
890 new.redact_keys(self.redacted_keys());
891 for key in keys {
892 if let Some(v) = self.get_raw(key) {
893 new.put_value((*key).to_string(), v, StateMutationOrigin::Set);
894 }
895 }
896 new
897 }
898
899 pub fn merge(&self, other: &State) {
901 for entry in other.inner.iter() {
902 self.put_value(
903 entry.key().clone(),
904 entry.value().clone(),
905 StateMutationOrigin::Set,
906 );
907 }
908 }
909
910 pub fn rename(&self, from: &str, to: &str) {
912 if let Some(v) = self.remove(from) {
913 self.put_value(to.to_string(), v, StateMutationOrigin::Set);
914 }
915 }
916
917 pub fn is_tracking_delta(&self) -> bool {
921 self.track_delta
922 }
923
924 pub fn has_delta(&self) -> bool {
926 self.track_delta && !self.delta.is_empty()
927 }
928
929 pub fn delta(&self) -> HashMap<String, Value> {
931 self.delta
932 .iter()
933 .filter_map(|entry| match entry.value() {
934 DeltaOp::Put(v) => Some((entry.key().clone(), v.clone())),
935 DeltaOp::Delete => None,
936 })
937 .collect()
938 }
939
940 pub fn commit(&self) {
945 let ops: Vec<(String, DeltaOp)> = self
947 .delta
948 .iter()
949 .map(|e| (e.key().clone(), e.value().clone()))
950 .collect();
951 for (key, op) in ops {
952 match op {
953 DeltaOp::Put(value) => {
954 let old = self.inner.insert(key.clone(), value.clone());
955 self.record_mutation_with_delta(
956 key,
957 old,
958 Some(value),
959 StateMutationOrigin::Commit,
960 false,
961 );
962 }
963 DeltaOp::Delete => {
964 if let Some((_, old)) = self.inner.remove(&key) {
965 self.record_mutation_with_delta(
966 key,
967 Some(old),
968 None,
969 StateMutationOrigin::Commit,
970 false,
971 );
972 }
973 }
974 }
975 }
976 self.delta.clear();
977 }
978
979 pub fn rollback(&self) {
985 self.delta.clear();
986 }
987
988 pub fn app(&self) -> PrefixedState<'_> {
992 PrefixedState {
993 state: self,
994 prefix: "app:",
995 }
996 }
997
998 pub fn user(&self) -> PrefixedState<'_> {
1000 PrefixedState {
1001 state: self,
1002 prefix: "user:",
1003 }
1004 }
1005
1006 pub fn temp(&self) -> PrefixedState<'_> {
1008 PrefixedState {
1009 state: self,
1010 prefix: "temp:",
1011 }
1012 }
1013
1014 pub fn session(&self) -> PrefixedState<'_> {
1016 PrefixedState {
1017 state: self,
1018 prefix: "session:",
1019 }
1020 }
1021
1022 pub fn turn(&self) -> PrefixedState<'_> {
1024 PrefixedState {
1025 state: self,
1026 prefix: "turn:",
1027 }
1028 }
1029
1030 pub fn bg(&self) -> PrefixedState<'_> {
1032 PrefixedState {
1033 state: self,
1034 prefix: "bg:",
1035 }
1036 }
1037
1038 pub fn derived(&self) -> ReadOnlyPrefixedState<'_> {
1040 ReadOnlyPrefixedState {
1041 state: self,
1042 prefix: "derived:",
1043 }
1044 }
1045
1046 pub fn snapshot_values(&self, keys: &[&str]) -> HashMap<String, Value> {
1051 keys.iter()
1052 .filter_map(|&k| self.get_raw(k).map(|v| (k.to_string(), v)))
1053 .collect()
1054 }
1055
1056 pub fn diff_values(
1059 &self,
1060 prev: &HashMap<String, Value>,
1061 keys: &[&str],
1062 ) -> Vec<(String, Value, Value)> {
1063 keys.iter()
1064 .filter_map(|&k| {
1065 let old = prev.get(k);
1066 let new = self.get_raw(k);
1067 match (old, new) {
1068 (Some(o), Some(n)) if o != &n => Some((k.to_string(), o.clone(), n)),
1069 (None, Some(n)) => Some((k.to_string(), Value::Null, n)),
1070 (Some(o), None) => Some((k.to_string(), o.clone(), Value::Null)),
1071 _ => None,
1072 }
1073 })
1074 .collect()
1075 }
1076
1077 pub fn to_hashmap(&self) -> std::collections::HashMap<String, serde_json::Value> {
1079 self.inner
1080 .iter()
1081 .map(|entry| (entry.key().clone(), entry.value().clone()))
1082 .collect()
1083 }
1084
1085 pub fn from_hashmap(&self, map: std::collections::HashMap<String, serde_json::Value>) {
1087 for (key, value) in map {
1088 let old = self.inner.insert(key.clone(), value.clone());
1090 self.record_mutation(key, old, Some(value), StateMutationOrigin::SetCommitted);
1091 }
1092 }
1093
1094 pub fn clear_prefix(&self, prefix: &str) {
1100 if self.track_delta {
1101 let keys: Vec<String> = self
1102 .keys()
1103 .into_iter()
1104 .filter(|k| k.starts_with(prefix))
1105 .collect();
1106 for key in keys {
1107 let old = self.get_raw(&key);
1108 self.delta.insert(key.clone(), DeltaOp::Delete);
1109 if let Some(old) = old {
1110 self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1111 }
1112 }
1113 return;
1114 }
1115 let keys_to_remove: Vec<String> = self
1116 .inner
1117 .iter()
1118 .filter(|entry| entry.key().starts_with(prefix))
1119 .map(|entry| entry.key().clone())
1120 .collect();
1121 for key in keys_to_remove {
1122 if let Some((_, old)) = self.inner.remove(&key) {
1123 self.record_mutation(key, Some(old), None, StateMutationOrigin::ClearPrefix);
1124 }
1125 }
1126 }
1127
1128 pub fn recent_mutations(&self) -> Vec<StateMutation> {
1130 self.mutations
1131 .lock()
1132 .expect("state mutation journal poisoned")
1133 .iter()
1134 .cloned()
1135 .collect()
1136 }
1137
1138 pub fn mutation_cursor(&self) -> u64 {
1140 self.next_mutation_sequence.load(Ordering::Relaxed) - 1
1141 }
1142
1143 pub fn mutations_since(&self, cursor: u64) -> Vec<StateMutation> {
1145 let mutations = self
1146 .mutations
1147 .lock()
1148 .expect("state mutation journal poisoned");
1149 mutations
1150 .iter()
1151 .filter(|mutation| mutation.sequence > cursor)
1152 .cloned()
1153 .collect()
1154 }
1155
1156 pub fn try_mutations_since(&self, cursor: u64) -> Option<Vec<StateMutation>> {
1163 let mutations = self
1164 .mutations
1165 .lock()
1166 .expect("state mutation journal poisoned");
1167 let latest = self.next_mutation_sequence.load(Ordering::Relaxed) - 1;
1168 if latest > cursor {
1169 let oldest = mutations.front().map_or(u64::MAX, |m| m.sequence);
1170 if oldest > cursor + 1 {
1171 return None;
1172 }
1173 }
1174 Some(
1175 mutations
1176 .iter()
1177 .filter(|mutation| mutation.sequence > cursor)
1178 .cloned()
1179 .collect(),
1180 )
1181 }
1182
1183 pub fn drain_mutations(&self) -> Vec<StateMutation> {
1185 self.mutations
1186 .lock()
1187 .expect("state mutation journal poisoned")
1188 .drain(..)
1189 .collect()
1190 }
1191
1192 pub fn evidence(&self, key: &str) -> SlotEvidence {
1195 let value = self.get_raw(key);
1196 let meta = self.get::<Value>(&format!("state_meta:{key}"));
1197 let source = meta
1198 .as_ref()
1199 .and_then(|m| m.get("source"))
1200 .and_then(|s| s.as_str().map(String::from));
1201 let confidence = meta
1202 .as_ref()
1203 .and_then(|m| m.get("confidence"))
1204 .and_then(serde_json::Value::as_f64);
1205
1206 let mut last_sequence: Option<u64> = None;
1207 let mut last_origin: Option<StateMutationOrigin> = None;
1208 for m in self.recent_mutations() {
1209 if m.key == key && last_sequence.is_none_or(|s| m.sequence > s) {
1210 last_sequence = Some(m.sequence);
1211 last_origin = Some(m.origin);
1212 }
1213 }
1214
1215 SlotEvidence {
1216 key: key.to_string(),
1217 present: value.is_some(),
1218 value,
1219 source,
1220 confidence,
1221 last_sequence,
1222 last_origin,
1223 }
1224 }
1225
1226 fn record_mutation(
1227 &self,
1228 key: String,
1229 old: Option<Value>,
1230 new: Option<Value>,
1231 origin: StateMutationOrigin,
1232 ) {
1233 self.record_mutation_with_delta(key, old, new, origin, self.track_delta);
1234 }
1235
1236 fn record_mutation_with_delta(
1237 &self,
1238 key: String,
1239 old: Option<Value>,
1240 new: Option<Value>,
1241 origin: StateMutationOrigin,
1242 delta: bool,
1243 ) {
1244 let mut mutations = self
1245 .mutations
1246 .lock()
1247 .expect("state mutation journal poisoned");
1248 if mutations.len() >= self.mutation_capacity {
1249 mutations.pop_front();
1250 }
1251 let sequence = self.next_mutation_sequence.fetch_add(1, Ordering::Relaxed);
1252 let mutation = StateMutation {
1253 sequence,
1254 key,
1255 old,
1256 new,
1257 origin,
1258 timestamp: self.clock().system_time(),
1259 delta,
1260 };
1261 if let Some(sink) = self.journal_sink.0.read().as_ref() {
1264 if self.is_redacted(&mutation.key) {
1265 let masked = |v: &Option<Value>| v.as_ref().map(|_| Value::String(REDACTED.into()));
1266 sink.write(&StateMutation {
1267 old: masked(&mutation.old),
1268 new: masked(&mutation.new),
1269 ..mutation.clone()
1270 });
1271 } else {
1272 sink.write(&mutation);
1273 }
1274 }
1275 mutations.push_back(mutation);
1276 }
1277}
1278
1279pub struct PrefixedState<'a> {
1281 state: &'a State,
1282 prefix: &'static str,
1283}
1284
1285impl<'a> PrefixedState<'a> {
1286 fn prefixed_key(&self, key: &str) -> String {
1287 format!("{}{}", self.prefix, key)
1288 }
1289
1290 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1292 self.state.get(&self.prefixed_key(key))
1293 }
1294
1295 pub fn get_raw(&self, key: &str) -> Option<Value> {
1297 self.state.get_raw(&self.prefixed_key(key))
1298 }
1299
1300 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1302 where
1303 F: FnOnce(&Value) -> R,
1304 {
1305 self.state.with(&self.prefixed_key(key), f)
1306 }
1307
1308 pub fn set(
1312 &self,
1313 key: impl AsRef<str>,
1314 value: impl serde::Serialize,
1315 ) -> Result<(), StateError> {
1316 self.state.set(self.prefixed_key(key.as_ref()), value)
1317 }
1318
1319 pub fn contains(&self, key: &str) -> bool {
1321 self.state.contains(&self.prefixed_key(key))
1322 }
1323
1324 pub fn remove(&self, key: &str) -> Option<Value> {
1326 self.state.remove(&self.prefixed_key(key))
1327 }
1328
1329 pub fn keys(&self) -> Vec<String> {
1331 self.state
1332 .keys()
1333 .into_iter()
1334 .filter_map(|k| {
1335 k.strip_prefix(self.prefix)
1336 .map(std::string::ToString::to_string)
1337 })
1338 .collect()
1339 }
1340}
1341
1342pub struct ReadOnlyPrefixedState<'a> {
1347 state: &'a State,
1348 prefix: &'static str,
1349}
1350
1351impl<'a> ReadOnlyPrefixedState<'a> {
1352 fn prefixed_key(&self, key: &str) -> String {
1353 format!("{}{}", self.prefix, key)
1354 }
1355
1356 pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
1358 self.state.get(&self.prefixed_key(key))
1359 }
1360
1361 pub fn get_raw(&self, key: &str) -> Option<Value> {
1363 self.state.get_raw(&self.prefixed_key(key))
1364 }
1365
1366 pub fn with<F, R>(&self, key: &str, f: F) -> Option<R>
1368 where
1369 F: FnOnce(&Value) -> R,
1370 {
1371 self.state.with(&self.prefixed_key(key), f)
1372 }
1373
1374 pub fn contains(&self, key: &str) -> bool {
1376 self.state.contains(&self.prefixed_key(key))
1377 }
1378
1379 pub fn keys(&self) -> Vec<String> {
1381 self.state
1382 .keys()
1383 .into_iter()
1384 .filter_map(|k| {
1385 k.strip_prefix(self.prefix)
1386 .map(std::string::ToString::to_string)
1387 })
1388 .collect()
1389 }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394 use super::*;
1395
1396 #[test]
1397 fn journal_sink_receives_every_mutation_in_ring_order() {
1398 let state = State::new();
1399 let sink = Arc::new(MemoryJournalSink::new());
1400 state.set_journal_sink(sink.clone());
1401
1402 let _ = state.set("a", 1);
1403 let _ = state.set("b", "two");
1404 state.remove("a");
1405
1406 let entries = sink.entries();
1407 assert_eq!(entries.len(), 3);
1408 assert_eq!(entries, state.recent_mutations());
1409 assert_eq!(entries[0].key, "a");
1410 assert_eq!(entries[2].origin, StateMutationOrigin::Remove);
1411 }
1412
1413 #[test]
1414 fn journal_sink_is_shared_with_clones_and_delta_views() {
1415 let state = State::new();
1416 let sink = Arc::new(MemoryJournalSink::new());
1417 state.set_journal_sink(sink.clone());
1418
1419 let clone = state.clone();
1420 let _ = clone.set("from_clone", true);
1421
1422 let tracked = state.with_delta_tracking();
1423 let _ = tracked.set("from_delta", 1);
1424 tracked.commit();
1425
1426 let keys: Vec<_> = sink.entries().iter().map(|m| m.key.clone()).collect();
1427 assert!(keys.contains(&"from_clone".to_string()));
1428 assert!(keys.contains(&"from_delta".to_string()));
1429 assert!(
1431 sink.entries()
1432 .iter()
1433 .any(|m| m.origin == StateMutationOrigin::Commit)
1434 );
1435 }
1436
1437 #[test]
1438 fn journal_sink_outlives_ring_capacity() {
1439 let state = State::new();
1441 let sink = Arc::new(MemoryJournalSink::new());
1442 state.set_journal_sink(sink.clone());
1443
1444 for i in 0..(DEFAULT_MUTATION_JOURNAL_CAPACITY + 10) {
1445 let _ = state.set(format!("k{i}"), i);
1446 }
1447
1448 assert_eq!(
1449 state.recent_mutations().len(),
1450 DEFAULT_MUTATION_JOURNAL_CAPACITY
1451 );
1452 assert_eq!(sink.len(), DEFAULT_MUTATION_JOURNAL_CAPACITY + 10);
1453 assert_eq!(sink.entries()[0].key, "k0");
1454 }
1455
1456 #[test]
1457 fn state_mutation_serde_round_trip_uses_epoch_millis() {
1458 let m = StateMutation {
1459 sequence: 42,
1460 key: "app:last_city".into(),
1461 old: None,
1462 new: Some(serde_json::json!("London")),
1463 origin: StateMutationOrigin::Set,
1464 timestamp: std::time::UNIX_EPOCH + std::time::Duration::from_millis(1_718_000_000_123),
1465 delta: false,
1466 };
1467 let json = serde_json::to_string(&m).unwrap();
1468 assert!(json.contains("\"timestamp_ms\":1718000000123"));
1469 assert!(json.contains("\"origin\":\"set\""));
1470 let back: StateMutation = serde_json::from_str(&json).unwrap();
1471 assert_eq!(back, m);
1472 }
1473
1474 #[test]
1475 fn file_journal_sink_round_trip() {
1476 let dir = std::env::temp_dir().join(format!(
1477 "gemini-rs-journal-test-{}-{}",
1478 std::process::id(),
1479 std::time::SystemTime::now()
1480 .duration_since(std::time::UNIX_EPOCH)
1481 .unwrap()
1482 .as_nanos()
1483 ));
1484 std::fs::create_dir_all(&dir).unwrap();
1485 let path = dir.join("session.journal.jsonl");
1486
1487 let state = State::new();
1488 {
1489 let sink = Arc::new(FileJournalSink::create(&path).unwrap());
1490 state.set_journal_sink(sink);
1491 let _ = state.set("a", 1);
1492 let _ = state.set("a", 2);
1493 state.remove("a");
1494 state.set_journal_sink(Arc::new(MemoryJournalSink::new()));
1496 }
1497
1498 let data = std::fs::read_to_string(&path).unwrap();
1499 let parsed: Vec<StateMutation> = data
1500 .lines()
1501 .filter(|l| !l.trim().is_empty())
1502 .map(|l| serde_json::from_str(l).unwrap())
1503 .collect();
1504 assert_eq!(parsed.len(), 3);
1505 assert_eq!(parsed[0].new, Some(serde_json::json!(1)));
1506 assert_eq!(parsed[2].origin, StateMutationOrigin::Remove);
1507
1508 let _ = std::fs::remove_dir_all(&dir);
1509 }
1510
1511 #[test]
1512 fn set_and_get_string() {
1513 let state = State::new();
1514 let _ = state.set("name", "Alice");
1515 assert_eq!(state.get::<String>("name"), Some("Alice".to_string()));
1516 }
1517
1518 #[test]
1519 fn set_and_get_json() {
1520 let state = State::new();
1521 let _ = state.set("data", serde_json::json!({"temp": 22}));
1522 let v: Value = state.get("data").unwrap();
1523 assert_eq!(v["temp"], 22);
1524 }
1525
1526 #[test]
1527 fn pick_subset() {
1528 let state = State::new();
1529 let _ = state.set("a", 1);
1530 let _ = state.set("b", 2);
1531 let _ = state.set("c", 3);
1532 let picked = state.pick(&["a", "c"]);
1533 assert!(picked.contains("a"));
1534 assert!(!picked.contains("b"));
1535 assert!(picked.contains("c"));
1536 }
1537
1538 #[test]
1539 fn merge_states() {
1540 let s1 = State::new();
1541 let _ = s1.set("a", 1);
1542 let s2 = State::new();
1543 let _ = s2.set("b", 2);
1544 s1.merge(&s2);
1545 assert!(s1.contains("a"));
1546 assert!(s1.contains("b"));
1547 }
1548
1549 #[test]
1550 fn rename_key() {
1551 let state = State::new();
1552 let _ = state.set("old", "value");
1553 state.rename("old", "new");
1554 assert!(!state.contains("old"));
1555 assert_eq!(state.get::<String>("new"), Some("value".to_string()));
1556 }
1557
1558 #[test]
1559 fn remove_returns_value() {
1560 let state = State::new();
1561 let _ = state.set("key", 42);
1562 let removed = state.remove("key");
1563 assert!(removed.is_some());
1564 assert!(!state.contains("key"));
1565 }
1566
1567 #[test]
1568 fn get_missing_returns_none() {
1569 let state = State::new();
1570 assert_eq!(state.get::<String>("nope"), None);
1571 }
1572
1573 #[test]
1576 fn delta_tracking_writes_to_delta() {
1577 let state = State::new();
1578 let _ = state.set("committed", "yes");
1579
1580 let tracked = state.with_delta_tracking();
1581 let _ = tracked.set("new_key", "new_value");
1582
1583 assert_eq!(
1585 tracked.get::<String>("new_key"),
1586 Some("new_value".to_string())
1587 );
1588 assert!(!state.contains("new_key"));
1590 assert_eq!(tracked.get::<String>("committed"), Some("yes".to_string()));
1592 }
1593
1594 #[test]
1595 fn delta_has_delta_reports_correctly() {
1596 let state = State::new();
1597 let tracked = state.with_delta_tracking();
1598 assert!(!tracked.has_delta());
1599
1600 let _ = tracked.set("key", "val");
1601 assert!(tracked.has_delta());
1602 }
1603
1604 #[test]
1605 fn delta_commit_merges_to_inner() {
1606 let state = State::new();
1607 let tracked = state.with_delta_tracking();
1608 let _ = tracked.set("key", "val");
1609 assert!(!state.contains("key"));
1610
1611 tracked.commit();
1612 assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1614 assert!(!tracked.has_delta());
1615 }
1616
1617 #[test]
1618 fn delta_rollback_discards_changes() {
1619 let state = State::new();
1620 let tracked = state.with_delta_tracking();
1621 let _ = tracked.set("key", "val");
1622 assert!(tracked.has_delta());
1623
1624 tracked.rollback();
1625 assert!(!tracked.has_delta());
1626 assert!(!state.contains("key"));
1627 assert!(!tracked.contains("key"));
1628 }
1629
1630 #[test]
1631 fn delta_snapshot() {
1632 let state = State::new();
1633 let tracked = state.with_delta_tracking();
1634 let _ = tracked.set("a", 1);
1635 let _ = tracked.set("b", 2);
1636
1637 let snapshot = tracked.delta();
1638 assert_eq!(snapshot.len(), 2);
1639 assert!(snapshot.contains_key("a"));
1640 assert!(snapshot.contains_key("b"));
1641 }
1642
1643 #[test]
1644 fn set_committed_bypasses_delta() {
1645 let state = State::new();
1646 let tracked = state.with_delta_tracking();
1647 let _ = tracked.set_committed("direct", "value");
1648
1649 assert_eq!(state.get::<String>("direct"), Some("value".to_string()));
1651 assert!(!tracked.has_delta());
1653 assert_eq!(tracked.get::<String>("direct"), Some("value".to_string()));
1655 }
1656
1657 #[test]
1658 fn mutation_journal_records_set_and_remove() {
1659 let state = State::new();
1660 let _ = state.set("key", "first");
1661 let _ = state.set("key", "second");
1662 state.remove("key");
1663
1664 let mutations = state.recent_mutations();
1665 assert_eq!(mutations.len(), 3);
1666 assert_eq!(mutations[0].key, "key");
1667 assert_eq!(mutations[0].old, None);
1668 assert_eq!(mutations[0].new, Some(serde_json::json!("first")));
1669 assert_eq!(mutations[0].origin, StateMutationOrigin::Set);
1670
1671 assert_eq!(mutations[1].old, Some(serde_json::json!("first")));
1672 assert_eq!(mutations[1].new, Some(serde_json::json!("second")));
1673
1674 assert_eq!(mutations[2].old, Some(serde_json::json!("second")));
1675 assert_eq!(mutations[2].new, None);
1676 assert_eq!(mutations[2].origin, StateMutationOrigin::Remove);
1677 }
1678
1679 #[test]
1680 fn mutation_journal_is_shared_with_delta_tracking() {
1681 let state = State::new();
1682 let _ = state.set("committed", "yes");
1683
1684 let tracked = state.with_delta_tracking();
1685 let _ = tracked.set("committed", "maybe");
1686 tracked.commit();
1687
1688 let mutations = state.recent_mutations();
1689 assert_eq!(mutations.len(), 3);
1690 assert_eq!(mutations[1].key, "committed");
1691 assert_eq!(mutations[1].old, Some(serde_json::json!("yes")));
1692 assert_eq!(mutations[1].new, Some(serde_json::json!("maybe")));
1693 assert_eq!(mutations[1].origin, StateMutationOrigin::Set);
1694 assert!(mutations[1].delta);
1695
1696 assert_eq!(mutations[2].origin, StateMutationOrigin::Commit);
1697 assert!(!mutations[2].delta);
1698 }
1699
1700 #[test]
1701 fn drain_mutations_clears_journal() {
1702 let state = State::new();
1703 let _ = state.set("a", 1);
1704 let _ = state.set("b", 2);
1705
1706 let drained = state.drain_mutations();
1707 assert_eq!(drained.len(), 2);
1708 assert!(state.recent_mutations().is_empty());
1709 }
1710
1711 #[test]
1712 fn redacted_keys_are_masked_wherever_state_leaves_the_process() {
1713 let sink = Arc::new(MemoryJournalSink::new());
1714 let state = State::new().with_journal_sink(sink.clone());
1715 state.redact_keys(["card_number"]);
1716 let _ = state.set("card_number", "4111111111111111");
1717 let _ = state.set("app:card_number", "4111111111111111");
1718 let _ = state.set("name", "Ada");
1719
1720 assert_eq!(
1722 state.get::<String>("card_number").as_deref(),
1723 Some("4111111111111111")
1724 );
1725 for entry in sink.entries() {
1727 let leaked = entry
1728 .new
1729 .as_ref()
1730 .is_some_and(|v| v.to_string().contains("4111"));
1731 assert!(!leaked, "{} leaked into the journal sink", entry.key);
1732 }
1733 let snapshot = state.to_redacted_hashmap();
1735 assert_eq!(snapshot["card_number"], serde_json::json!(REDACTED));
1736 assert_eq!(snapshot["app:card_number"], serde_json::json!(REDACTED));
1737 assert_eq!(snapshot["name"], serde_json::json!("Ada"));
1738 assert_eq!(
1740 state.redact_fields(&serde_json::json!({ "card_number": "4111", "name": "Ada" })),
1741 serde_json::json!({ "card_number": REDACTED, "name": "Ada" })
1742 );
1743 assert!(
1745 state
1746 .recent_mutations()
1747 .iter()
1748 .any(|m| m.new == Some(serde_json::json!("4111111111111111")))
1749 );
1750 }
1751
1752 #[test]
1753 fn try_mutations_since_reports_a_gap_the_ring_dropped() {
1754 let state = State::new();
1755 let cursor = state.mutation_cursor();
1756 let _ = state.set("a", 1);
1757 assert_eq!(state.try_mutations_since(cursor).map(|m| m.len()), Some(1));
1758
1759 state.drain_mutations();
1760 let _ = state.set("b", 2);
1761 assert!(
1762 state.try_mutations_since(cursor).is_none(),
1763 "the write to `a` is gone from the journal"
1764 );
1765 let after_drain = state.mutation_cursor() - 1;
1766 assert_eq!(
1767 state.try_mutations_since(after_drain).map(|m| m.len()),
1768 Some(1)
1769 );
1770 assert_eq!(
1771 state
1772 .try_mutations_since(state.mutation_cursor())
1773 .map(|m| m.len()),
1774 Some(0)
1775 );
1776 }
1777
1778 #[test]
1779 fn mutation_cursor_reads_only_later_changes() {
1780 let state = State::new();
1781 let _ = state.set("before", 1);
1782 let cursor = state.mutation_cursor();
1783
1784 let _ = state.set("after", 2);
1785 state.remove("before");
1786
1787 let mutations = state.mutations_since(cursor);
1788 assert_eq!(mutations.len(), 2);
1789 assert_eq!(mutations[0].key, "after");
1790 assert_eq!(mutations[1].key, "before");
1791 }
1792
1793 #[test]
1794 fn no_delta_tracking_preserves_existing_behavior() {
1795 let state = State::new();
1796 assert!(!state.is_tracking_delta());
1797 let _ = state.set("key", "val");
1798 assert_eq!(state.get::<String>("key"), Some("val".to_string()));
1799 assert!(!state.has_delta());
1800 }
1801
1802 #[test]
1805 fn prefix_app_set_and_get() {
1806 let state = State::new();
1807 let _ = state.app().set("flag", true);
1808
1809 assert_eq!(state.app().get::<bool>("flag"), Some(true));
1811 assert_eq!(state.get::<bool>("app:flag"), Some(true));
1813 }
1814
1815 #[test]
1816 fn prefix_user_set_and_get() {
1817 let state = State::new();
1818 let _ = state.user().set("name", "Alice");
1819 assert_eq!(
1820 state.user().get::<String>("name"),
1821 Some("Alice".to_string())
1822 );
1823 assert_eq!(state.get::<String>("user:name"), Some("Alice".to_string()));
1824 }
1825
1826 #[test]
1827 fn prefix_temp_set_and_get() {
1828 let state = State::new();
1829 let _ = state.temp().set("scratch", 42);
1830 assert_eq!(state.temp().get::<i32>("scratch"), Some(42));
1831 }
1832
1833 #[test]
1834 fn prefix_contains_and_remove() {
1835 let state = State::new();
1836 let _ = state.app().set("x", 1);
1837 assert!(state.app().contains("x"));
1838 state.app().remove("x");
1839 assert!(!state.app().contains("x"));
1840 }
1841
1842 #[test]
1843 fn prefix_keys() {
1844 let state = State::new();
1845 let _ = state.app().set("a", 1);
1846 let _ = state.app().set("b", 2);
1847 let _ = state.user().set("c", 3);
1848
1849 let app_keys = state.app().keys();
1850 assert_eq!(app_keys.len(), 2);
1851 assert!(app_keys.contains(&"a".to_string()));
1852 assert!(app_keys.contains(&"b".to_string()));
1853
1854 let user_keys = state.user().keys();
1855 assert_eq!(user_keys.len(), 1);
1856 assert!(user_keys.contains(&"c".to_string()));
1857 }
1858
1859 #[test]
1860 fn prefix_with_delta_tracking() {
1861 let state = State::new();
1862 let tracked = state.with_delta_tracking();
1863 let _ = tracked.app().set("flag", true);
1864
1865 assert_eq!(tracked.app().get::<bool>("flag"), Some(true));
1867 assert!(tracked.has_delta());
1869 assert!(!state.contains("app:flag"));
1870
1871 tracked.commit();
1872 assert_eq!(state.get::<bool>("app:flag"), Some(true));
1873 }
1874
1875 #[test]
1878 fn prefix_session_set_and_get() {
1879 let state = State::new();
1880 let _ = state.session().set("turn_count", 5);
1881 assert_eq!(state.session().get::<i32>("turn_count"), Some(5));
1882 assert_eq!(state.get::<i32>("session:turn_count"), Some(5));
1883 }
1884
1885 #[test]
1886 fn prefix_turn_set_and_get() {
1887 let state = State::new();
1888 let _ = state.turn().set("transcript", "hello");
1889 assert_eq!(
1890 state.turn().get::<String>("transcript"),
1891 Some("hello".to_string())
1892 );
1893 assert_eq!(
1894 state.get::<String>("turn:transcript"),
1895 Some("hello".to_string())
1896 );
1897 }
1898
1899 #[test]
1900 fn prefix_bg_set_and_get() {
1901 let state = State::new();
1902 let _ = state.bg().set("task_id", "abc-123");
1903 assert_eq!(
1904 state.bg().get::<String>("task_id"),
1905 Some("abc-123".to_string())
1906 );
1907 assert_eq!(
1908 state.get::<String>("bg:task_id"),
1909 Some("abc-123".to_string())
1910 );
1911 }
1912
1913 #[test]
1914 fn prefix_session_contains_and_remove() {
1915 let state = State::new();
1916 let _ = state.session().set("x", 1);
1917 assert!(state.session().contains("x"));
1918 state.session().remove("x");
1919 assert!(!state.session().contains("x"));
1920 }
1921
1922 #[test]
1923 fn prefix_turn_keys() {
1924 let state = State::new();
1925 let _ = state.turn().set("a", 1);
1926 let _ = state.turn().set("b", 2);
1927 let _ = state.session().set("c", 3);
1928
1929 let turn_keys = state.turn().keys();
1930 assert_eq!(turn_keys.len(), 2);
1931 assert!(turn_keys.contains(&"a".to_string()));
1932 assert!(turn_keys.contains(&"b".to_string()));
1933 }
1934
1935 #[test]
1938 fn try_get_distinguishes_absent_from_wrong_type() {
1939 let state = State::new();
1940 assert!(matches!(state.try_get::<u32>("missing"), Ok(None)));
1941
1942 state.set("n", 5u32).unwrap();
1943 assert_eq!(state.try_get::<u32>("n").unwrap(), Some(5));
1944
1945 state.set("s", "not a number").unwrap();
1946 assert_eq!(state.get::<u32>("s"), None);
1948 match state.try_get::<u32>("s") {
1950 Err(StateError::WrongType { key, .. }) => assert_eq!(key, "s"),
1951 other => panic!("expected WrongType, got {other:?}"),
1952 }
1953
1954 state.set("derived:risk", 0.5f64).unwrap();
1956 assert_eq!(state.try_get::<f64>("risk").unwrap(), Some(0.5));
1957
1958 const N: StateKey<u32> = StateKey::new("n");
1959 assert_eq!(state.try_get_key(&N).unwrap(), Some(5));
1960 const S: StateKey<u32> = StateKey::new("s");
1961 assert!(state.try_get_key(&S).is_err());
1962 }
1963
1964 #[test]
1967 fn derived_read_only_get() {
1968 let state = State::new();
1969 let _ = state.set("derived:sentiment", "positive");
1971 assert_eq!(
1972 state.derived().get::<String>("sentiment"),
1973 Some("positive".to_string())
1974 );
1975 }
1976
1977 #[test]
1978 fn derived_read_only_get_raw() {
1979 let state = State::new();
1980 let _ = state.set("derived:score", serde_json::json!(0.95));
1981 let raw = state.derived().get_raw("score");
1982 assert!(raw.is_some());
1983 assert_eq!(raw.unwrap(), serde_json::json!(0.95));
1984 }
1985
1986 #[test]
1987 fn derived_read_only_contains() {
1988 let state = State::new();
1989 let _ = state.set("derived:exists", true);
1990 assert!(state.derived().contains("exists"));
1991 assert!(!state.derived().contains("missing"));
1992 }
1993
1994 #[test]
1995 fn derived_read_only_keys() {
1996 let state = State::new();
1997 let _ = state.set("derived:a", 1);
1998 let _ = state.set("derived:b", 2);
1999 let _ = state.set("app:c", 3);
2000
2001 let derived_keys = state.derived().keys();
2002 assert_eq!(derived_keys.len(), 2);
2003 assert!(derived_keys.contains(&"a".to_string()));
2004 assert!(derived_keys.contains(&"b".to_string()));
2005 }
2006
2007 #[test]
2008 fn derived_missing_key_returns_none() {
2009 let state = State::new();
2010 assert_eq!(state.derived().get::<String>("nope"), None);
2011 assert_eq!(state.derived().get_raw("nope"), None);
2012 }
2013
2014 #[test]
2017 fn snapshot_values_captures_existing_keys() {
2018 let state = State::new();
2019 let _ = state.set("a", 1);
2020 let _ = state.set("b", "hello");
2021 let _ = state.set("c", true);
2022
2023 let snap = state.snapshot_values(&["a", "b", "missing"]);
2024 assert_eq!(snap.len(), 2);
2025 assert_eq!(snap.get("a"), Some(&serde_json::json!(1)));
2026 assert_eq!(snap.get("b"), Some(&serde_json::json!("hello")));
2027 assert!(!snap.contains_key("missing"));
2028 }
2029
2030 #[test]
2031 fn snapshot_values_empty_keys() {
2032 let state = State::new();
2033 let _ = state.set("a", 1);
2034 let snap = state.snapshot_values(&[]);
2035 assert!(snap.is_empty());
2036 }
2037
2038 #[test]
2041 fn diff_values_detects_changed_value() {
2042 let state = State::new();
2043 let _ = state.set("x", 1);
2044 let snap = state.snapshot_values(&["x"]);
2045
2046 let _ = state.set("x", 2);
2047 let diffs = state.diff_values(&snap, &["x"]);
2048 assert_eq!(diffs.len(), 1);
2049 assert_eq!(diffs[0].0, "x");
2050 assert_eq!(diffs[0].1, serde_json::json!(1));
2051 assert_eq!(diffs[0].2, serde_json::json!(2));
2052 }
2053
2054 #[test]
2055 fn diff_values_detects_new_key() {
2056 let state = State::new();
2057 let snap = state.snapshot_values(&["y"]);
2058
2059 let _ = state.set("y", "new");
2060 let diffs = state.diff_values(&snap, &["y"]);
2061 assert_eq!(diffs.len(), 1);
2062 assert_eq!(diffs[0].0, "y");
2063 assert_eq!(diffs[0].1, Value::Null);
2064 assert_eq!(diffs[0].2, serde_json::json!("new"));
2065 }
2066
2067 #[test]
2068 fn diff_values_detects_removed_key() {
2069 let state = State::new();
2070 let _ = state.set("z", 42);
2071 let snap = state.snapshot_values(&["z"]);
2072
2073 state.remove("z");
2074 let diffs = state.diff_values(&snap, &["z"]);
2075 assert_eq!(diffs.len(), 1);
2076 assert_eq!(diffs[0].0, "z");
2077 assert_eq!(diffs[0].1, serde_json::json!(42));
2078 assert_eq!(diffs[0].2, Value::Null);
2079 }
2080
2081 #[test]
2082 fn diff_values_no_change() {
2083 let state = State::new();
2084 let _ = state.set("stable", 10);
2085 let snap = state.snapshot_values(&["stable"]);
2086
2087 let diffs = state.diff_values(&snap, &["stable"]);
2089 assert!(diffs.is_empty());
2090 }
2091
2092 #[test]
2093 fn diff_values_multiple_keys_mixed_changes() {
2094 let state = State::new();
2095 let _ = state.set("a", 1);
2096 let _ = state.set("b", 2);
2097 let snap = state.snapshot_values(&["a", "b", "c"]);
2098
2099 let _ = state.set("a", 10); let _ = state.set("c", 3); let diffs = state.diff_values(&snap, &["a", "b", "c"]);
2104 assert_eq!(diffs.len(), 2); let diff_keys: Vec<&str> = diffs.iter().map(|(k, _, _)| k.as_str()).collect();
2106 assert!(diff_keys.contains(&"a"));
2107 assert!(diff_keys.contains(&"c"));
2108 }
2109
2110 #[test]
2113 fn clear_prefix_removes_matching_keys() {
2114 let state = State::new();
2115 let _ = state.set("turn:a", 1);
2116 let _ = state.set("turn:b", 2);
2117 let _ = state.set("app:c", 3);
2118 let _ = state.set("session:d", 4);
2119
2120 state.clear_prefix("turn:");
2121 assert!(!state.contains("turn:a"));
2122 assert!(!state.contains("turn:b"));
2123 assert!(state.contains("app:c"));
2124 assert!(state.contains("session:d"));
2125 }
2126
2127 #[test]
2128 fn clear_prefix_no_matching_keys_is_noop() {
2129 let state = State::new();
2130 let _ = state.set("app:x", 1);
2131 state.clear_prefix("turn:");
2132 assert!(state.contains("app:x"));
2133 }
2134
2135 #[test]
2136 fn clear_prefix_also_clears_delta() {
2137 let state = State::new();
2138 let _ = state.set("turn:committed", 1);
2139 let tracked = state.with_delta_tracking();
2140 let _ = tracked.set("turn:delta_val", 2);
2141
2142 assert!(tracked.contains("turn:committed"));
2144 assert!(tracked.contains("turn:delta_val"));
2145
2146 tracked.clear_prefix("turn:");
2147 assert!(!tracked.contains("turn:committed"));
2148 assert!(!tracked.contains("turn:delta_val"));
2149 }
2150
2151 #[test]
2152 fn clear_prefix_via_turn_accessor() {
2153 let state = State::new();
2154 let _ = state.turn().set("x", 1);
2155 let _ = state.turn().set("y", 2);
2156 let _ = state.app().set("z", 3);
2157
2158 state.clear_prefix("turn:");
2159 assert!(state.turn().keys().is_empty());
2160 assert!(state.app().contains("z"));
2161 }
2162
2163 #[test]
2166 fn modify_increment_existing() {
2167 let state = State::new();
2168 let _ = state.set("count", 5u32);
2169 let result = state.modify("count", 0u32, |n| n + 1).unwrap();
2170 assert_eq!(result, 6);
2171 assert_eq!(state.get::<u32>("count"), Some(6));
2172 }
2173
2174 #[test]
2175 fn modify_uses_default_when_missing() {
2176 let state = State::new();
2177 let result = state.modify("new_count", 0u32, |n| n + 1).unwrap();
2178 assert_eq!(result, 1);
2179 assert_eq!(state.get::<u32>("new_count"), Some(1));
2180 }
2181
2182 #[test]
2183 fn modify_with_delta_tracking() {
2184 let state = State::new();
2185 let _ = state.set("x", 10u32);
2186 let tracked = state.with_delta_tracking();
2187 let result = tracked.modify("x", 0u32, |n| n * 2).unwrap();
2188 assert_eq!(result, 20);
2189 assert_eq!(tracked.get::<u32>("x"), Some(20));
2191 assert_eq!(state.get::<u32>("x"), Some(10)); }
2193
2194 #[test]
2197 fn get_falls_back_to_derived_prefix() {
2198 let state = State::new();
2199 let _ = state.set("derived:risk", 0.85);
2200 assert_eq!(state.get::<f64>("risk"), Some(0.85));
2202 }
2203
2204 #[test]
2205 fn get_prefers_direct_key_over_derived() {
2206 let state = State::new();
2207 let _ = state.set("score", 1.0);
2208 let _ = state.set("derived:score", 0.5);
2209 assert_eq!(state.get::<f64>("score"), Some(1.0));
2211 }
2212
2213 #[test]
2214 fn get_derived_fallback_skipped_for_prefixed_keys() {
2215 let state = State::new();
2216 let _ = state.set("derived:risk", 0.85);
2217 assert_eq!(state.get::<f64>("app:risk"), None);
2219 }
2220
2221 #[test]
2222 fn get_derived_fallback_with_delta_tracking() {
2223 let state = State::new();
2224 let tracked = state.with_delta_tracking();
2225 let _ = tracked.set("derived:computed_val", 42);
2226 assert_eq!(tracked.get::<i32>("computed_val"), Some(42));
2227 }
2228
2229 #[test]
2232 fn with_reads_from_inner() {
2233 let state = State::new();
2234 let _ = state.set("name", "Alice");
2235 let len = state.with("name", |v| v.as_str().unwrap().len());
2236 assert_eq!(len, Some(5));
2237 }
2238
2239 #[test]
2240 fn with_reads_from_delta_first() {
2241 let state = State::new();
2242 let _ = state.set("x", 1);
2243 let tracked = state.with_delta_tracking();
2244 let _ = tracked.set("x", 99);
2245 let val = tracked.with("x", |v| v.as_i64().unwrap());
2246 assert_eq!(val, Some(99));
2247 }
2248
2249 #[test]
2250 fn with_falls_back_to_inner_when_not_in_delta() {
2251 let state = State::new();
2252 let _ = state.set("committed", "yes");
2253 let tracked = state.with_delta_tracking();
2254 let val = tracked.with("committed", |v| v.as_str().unwrap().to_string());
2255 assert_eq!(val, Some("yes".to_string()));
2256 }
2257
2258 #[test]
2259 fn with_falls_back_to_derived() {
2260 let state = State::new();
2261 let _ = state.set("derived:risk", 0.85);
2262 let val = state.with("risk", |v| v.as_f64().unwrap());
2263 assert_eq!(val, Some(0.85));
2264 }
2265
2266 #[test]
2267 fn with_derived_fallback_skipped_for_prefixed() {
2268 let state = State::new();
2269 let _ = state.set("derived:risk", 0.85);
2270 let val = state.with("app:risk", |v| v.as_f64().unwrap());
2271 assert_eq!(val, None);
2272 }
2273
2274 #[test]
2275 fn with_returns_none_for_missing() {
2276 let state = State::new();
2277 let val = state.with("missing", std::clone::Clone::clone);
2278 assert_eq!(val, None);
2279 }
2280
2281 #[test]
2282 fn with_on_prefixed_state() {
2283 let state = State::new();
2284 let _ = state.app().set("flag", true);
2285 let val = state.app().with("flag", |v| v.as_bool().unwrap());
2286 assert_eq!(val, Some(true));
2287 }
2288
2289 #[test]
2290 fn with_on_read_only_prefixed_state() {
2291 let state = State::new();
2292 let _ = state.set("derived:score", serde_json::json!(0.95));
2293 let val = state.derived().with("score", |v| v.as_f64().unwrap());
2294 assert_eq!(val, Some(0.95));
2295 }
2296
2297 const TURN_COUNT: StateKey<u32> = StateKey::new("session:turn_count");
2300 const NAME: StateKey<String> = StateKey::new("user:name");
2301
2302 #[test]
2303 fn state_key_get_and_set() {
2304 let state = State::new();
2305 let _ = state.set_key(&TURN_COUNT, 5);
2306 assert_eq!(state.get_key(&TURN_COUNT), Some(5));
2307 }
2308
2309 #[test]
2310 fn state_key_get_missing() {
2311 let state = State::new();
2312 assert_eq!(state.get_key(&TURN_COUNT), None);
2313 }
2314
2315 #[test]
2316 fn state_key_string_type() {
2317 let state = State::new();
2318 let _ = state.set_key(&NAME, "Alice".to_string());
2319 assert_eq!(state.get_key(&NAME), Some("Alice".to_string()));
2320 }
2321
2322 #[test]
2323 fn state_key_with() {
2324 let state = State::new();
2325 let _ = state.set_key(&TURN_COUNT, 42);
2326 let val = state.with_key(&TURN_COUNT, |v| v.as_u64().unwrap());
2327 assert_eq!(val, Some(42));
2328 }
2329
2330 #[test]
2331 fn state_key_interop_with_raw() {
2332 let state = State::new();
2333 let _ = state.set_key(&TURN_COUNT, 10);
2334 assert_eq!(state.get::<u32>("session:turn_count"), Some(10));
2336 }
2337
2338 #[test]
2339 fn slot_evidence_aggregates_value_provenance_and_journal() {
2340 let state = State::new();
2341 let _ = state.set("party_size", 6u8);
2342 let _ = state.set(
2344 "state_meta:party_size",
2345 serde_json::json!({ "source": "extraction", "confidence": 0.9 }),
2346 );
2347
2348 let ev = state.evidence("party_size");
2349 assert!(ev.present);
2350 assert_eq!(ev.value, Some(serde_json::json!(6)));
2351 assert_eq!(ev.source.as_deref(), Some("extraction"));
2352 assert_eq!(ev.confidence, Some(0.9));
2353 assert!(ev.last_sequence.is_some());
2354 assert_eq!(ev.last_origin, Some(StateMutationOrigin::Set));
2355
2356 let missing = state.evidence("nope");
2358 assert!(!missing.present);
2359 assert!(missing.source.is_none());
2360 }
2361
2362 #[test]
2365 fn rollback_restores_base_after_remove() {
2366 let base = State::new();
2369 let _ = base.set("k", "original");
2370
2371 let tx = base.with_delta_tracking();
2372 assert_eq!(tx.remove("k"), Some(serde_json::json!("original")));
2373 assert_eq!(tx.get::<String>("k"), None); assert_eq!(base.get::<String>("k"), Some("original".into())); tx.rollback();
2377 assert_eq!(tx.get::<String>("k"), Some("original".into()));
2378 assert_eq!(base.get::<String>("k"), Some("original".into()));
2379 }
2380
2381 #[test]
2382 fn rollback_restores_base_after_clear_prefix() {
2383 let base = State::new();
2385 let _ = base.set("app:a", 1u32);
2386 let _ = base.set("app:b", 2u32);
2387 let _ = base.set("user:c", 3u32);
2388
2389 let tx = base.with_delta_tracking();
2390 tx.clear_prefix("app:");
2391 assert_eq!(tx.get::<u32>("app:a"), None);
2392 assert_eq!(tx.get::<u32>("app:b"), None);
2393 assert_eq!(tx.get::<u32>("user:c"), Some(3));
2394 assert_eq!(base.get::<u32>("app:a"), Some(1));
2396
2397 tx.rollback();
2398 assert_eq!(tx.get::<u32>("app:a"), Some(1));
2399 assert_eq!(tx.get::<u32>("app:b"), Some(2));
2400 }
2401
2402 #[test]
2403 fn commit_applies_removals() {
2404 let base = State::new();
2405 let _ = base.set("k", "v");
2406 let tx = base.with_delta_tracking();
2407 tx.remove("k");
2408 tx.commit();
2409 assert_eq!(base.get::<String>("k"), None);
2410 }
2411
2412 #[test]
2413 fn commit_applies_prefix_clear() {
2414 let base = State::new();
2415 let _ = base.set("app:a", 1u32);
2416 let _ = base.set("user:c", 3u32);
2417 let tx = base.with_delta_tracking();
2418 tx.clear_prefix("app:");
2419 tx.commit();
2420 assert_eq!(base.get::<u32>("app:a"), None);
2421 assert_eq!(base.get::<u32>("user:c"), Some(3));
2422 }
2423
2424 #[test]
2425 fn modify_is_atomic_under_concurrency() {
2426 use std::sync::Arc;
2427 use std::thread;
2428
2429 let state = Arc::new(State::new());
2430 let _ = state.set("count", 0u64);
2431
2432 let threads = 8;
2433 let per_thread = 1000;
2434 let handles: Vec<_> = (0..threads)
2435 .map(|_| {
2436 let state = state.clone();
2437 thread::spawn(move || {
2438 for _ in 0..per_thread {
2439 let _ = state.modify("count", 0u64, |n| n + 1);
2440 }
2441 })
2442 })
2443 .collect();
2444 for h in handles {
2445 h.join().unwrap();
2446 }
2447 assert_eq!(
2449 state.get::<u64>("count"),
2450 Some((threads * per_thread) as u64)
2451 );
2452 }
2453}
2454
2455#[cfg(test)]
2456mod proptests {
2457 use super::*;
2458 use proptest::prelude::*;
2459
2460 proptest! {
2463 #[test]
2464 fn rollback_always_restores_base(
2465 base_keys in proptest::collection::vec(("[a-c]", 0u32..5), 0..6),
2466 ops in proptest::collection::vec(
2467 prop_oneof![
2468 ("[a-c]", 0u32..5).prop_map(|(k, v)| (k, Some(v))),
2469 "[a-c]".prop_map(|k| (k, None)),
2470 ],
2471 0..12,
2472 ),
2473 ) {
2474 let base = State::new();
2475 for (k, v) in &base_keys {
2476 let _ = base.set(k.clone(), *v);
2477 }
2478 let snapshot = |s: &State| -> std::collections::BTreeMap<String, Value> {
2479 s.keys().into_iter().filter_map(|k| s.get_raw(&k).map(|v| (k, v))).collect()
2480 };
2481 let before = snapshot(&base);
2482
2483 let tx = base.with_delta_tracking();
2484 for (k, v) in &ops {
2485 match v {
2486 Some(v) => { let _ = tx.set(k.clone(), *v); }
2487 None => { tx.remove(k); }
2488 }
2489 }
2490 prop_assert_eq!(&before, &snapshot(&base));
2492
2493 tx.rollback();
2494 prop_assert_eq!(&before, &snapshot(&tx));
2495 }
2496 }
2497}
2498
2499#[cfg(test)]
2500mod derived_contains_fallback {
2501 use super::State;
2506
2507 #[test]
2508 fn contains_sees_a_derived_value_through_the_unprefixed_key() {
2509 let state = State::new();
2510 state.set("derived:risk", 0.85).unwrap();
2511 assert_eq!(
2512 state.get::<f64>("risk"),
2513 Some(0.85),
2514 "precondition: get falls back"
2515 );
2516 assert!(
2517 state.contains("risk"),
2518 "contains must fall back the same way get does"
2519 );
2520 }
2521
2522 #[test]
2523 fn contains_fallback_respects_delta_tracking_and_tombstones() {
2524 let state = State::new();
2525 state.set("derived:score", 1u32).unwrap();
2526 let tracked = state.with_delta_tracking();
2527 assert!(
2528 tracked.contains("score"),
2529 "inner derived value visible through tracked view"
2530 );
2531 tracked.remove("derived:score");
2532 assert!(
2533 !tracked.contains("score"),
2534 "a tombstone on the derived key shadows inner"
2535 );
2536 }
2537
2538 #[test]
2539 fn contains_does_not_fall_back_for_prefixed_keys() {
2540 let state = State::new();
2541 state.set("derived:flag", true).unwrap();
2542 assert!(
2543 !state.contains("session:flag"),
2544 "only unprefixed keys get the fallback"
2545 );
2546 }
2547}