arkhe_forge_platform/
dedup.rs1use std::collections::HashMap;
10
11use arkhe_forge_core::context::IdempotencyIndex;
12use arkhe_kernel::abi::{EntityId, Tick};
13
14#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum DedupError {
19 #[error("idempotency key conflict: {0:x?}")]
21 Conflict([u8; 16]),
22}
23
24#[derive(Debug, Default)]
28pub struct InMemoryIdempotencyIndex {
29 inner: HashMap<[u8; 16], (EntityId, Tick)>,
30}
31
32impl InMemoryIdempotencyIndex {
33 #[inline]
35 #[must_use]
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn insert(
45 &mut self,
46 key: [u8; 16],
47 entity: EntityId,
48 tick: Tick,
49 ) -> Result<(), DedupError> {
50 if let Some(existing) = self.inner.get(&key) {
51 if *existing == (entity, tick) {
52 return Ok(());
53 }
54 return Err(DedupError::Conflict(key));
55 }
56 self.inner.insert(key, (entity, tick));
57 Ok(())
58 }
59
60 #[inline]
62 #[must_use]
63 pub fn len(&self) -> usize {
64 self.inner.len()
65 }
66
67 #[inline]
69 #[must_use]
70 pub fn is_empty(&self) -> bool {
71 self.inner.is_empty()
72 }
73}
74
75impl IdempotencyIndex for InMemoryIdempotencyIndex {
76 fn lookup(&self, key: &[u8; 16]) -> Option<(EntityId, Tick)> {
77 self.inner.get(key).copied()
78 }
79}
80
81#[cfg(test)]
82#[allow(clippy::unwrap_used, clippy::expect_used)]
83mod tests {
84 use super::*;
85
86 fn ent(v: u64) -> EntityId {
87 EntityId::new(v).unwrap()
88 }
89
90 #[test]
91 fn empty_store_reports_no_hit() {
92 let idx = InMemoryIdempotencyIndex::new();
93 assert!(idx.is_empty());
94 assert!(idx.lookup(&[0u8; 16]).is_none());
95 }
96
97 #[test]
98 fn insert_then_lookup_returns_same_binding() {
99 let mut idx = InMemoryIdempotencyIndex::new();
100 let key = [0x11u8; 16];
101 idx.insert(key, ent(42), Tick(100)).unwrap();
102 assert_eq!(idx.lookup(&key), Some((ent(42), Tick(100))));
103 }
104
105 #[test]
106 fn reinsert_same_binding_is_noop_success() {
107 let mut idx = InMemoryIdempotencyIndex::new();
108 let key = [0x22u8; 16];
109 idx.insert(key, ent(7), Tick(1)).unwrap();
110 idx.insert(key, ent(7), Tick(1)).unwrap();
111 assert_eq!(idx.len(), 1);
112 }
113
114 #[test]
115 fn conflict_returns_error() {
116 let mut idx = InMemoryIdempotencyIndex::new();
117 let key = [0x33u8; 16];
118 idx.insert(key, ent(1), Tick(1)).unwrap();
119 let err = idx.insert(key, ent(2), Tick(1)).unwrap_err();
120 assert!(matches!(err, DedupError::Conflict(k) if k == key));
121 }
122
123 #[test]
124 fn trait_object_usable_for_action_context() {
125 let mut idx = InMemoryIdempotencyIndex::new();
126 idx.insert([0x44u8; 16], ent(99), Tick(5)).unwrap();
127 let trait_ref: &dyn IdempotencyIndex = &idx;
128 assert_eq!(trait_ref.lookup(&[0x44u8; 16]), Some((ent(99), Tick(5))),);
129 }
130}