1use crate::state::{Durable, Kind, now_ms};
10use crate::store::StoreError;
11use serde::{Deserialize, Serialize};
12use serde_json::{Value, json};
13use std::collections::BTreeMap;
14
15pub const INDEX_ID: &str = "_index";
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct Record {
21 pub value: Value,
22 pub ts: u64,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub ttl_ms: Option<u64>,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub by: Option<String>,
27}
28
29impl Record {
30 pub fn expired(&self, now: u64) -> bool {
31 self.ttl_ms
32 .is_some_and(|ttl| now >= self.ts.saturating_add(ttl))
33 }
34 pub fn meta(&self) -> Value {
35 json!({"ts": self.ts, "ttl_ms": self.ttl_ms, "by": self.by})
36 }
37}
38
39pub struct Memory {
41 max_value_bytes: usize,
42 list_default_limit: usize,
43 index: Option<BTreeMap<String, u64>>,
45 probed: bool,
46}
47
48impl Memory {
49 pub fn new(max_value_bytes: usize, list_default_limit: usize) -> Memory {
50 Memory {
51 max_value_bytes,
52 list_default_limit,
53 index: None,
54 probed: false,
55 }
56 }
57
58 pub fn check_key(key: &str) -> Result<(), String> {
60 if key.is_empty() || key.len() > 256 {
61 return Err("memory key must be 1..=256 chars".into());
62 }
63 if key.starts_with('_') {
64 return Err("memory keys starting with '_' are reserved".into());
65 }
66 if key.chars().any(char::is_whitespace) {
67 return Err("memory key must not contain whitespace".into());
68 }
69 Ok(())
70 }
71
72 fn probe(&mut self, d: &Durable) {
73 if self.probed {
74 return;
75 }
76 self.probed = true;
77 match d.list(Kind::Memory) {
78 Ok(_) => self.index = None,
79 Err(StoreError::Unsupported(_)) => {
80 let mut idx = BTreeMap::new();
81 if let Ok(Some(env)) = d.get(Kind::Memory, INDEX_ID)
82 && let Some(m) = env.state.get("keys").and_then(Value::as_object)
83 {
84 for (k, v) in m {
85 idx.insert(k.clone(), v.as_u64().unwrap_or(0));
86 }
87 }
88 self.index = Some(idx);
89 }
90 Err(_) => {}
91 }
92 }
93
94 fn write_index(&self, d: &Durable) -> Result<(), StoreError> {
95 if let Some(idx) = &self.index {
96 d.put(Kind::Memory, INDEX_ID, json!({"keys": idx}), None)?;
97 }
98 Ok(())
99 }
100
101 pub fn set(
103 &mut self,
104 d: &Durable,
105 key: &str,
106 value: Value,
107 ttl_ms: Option<u64>,
108 by: Option<&str>,
109 ) -> Result<Value, String> {
110 Self::check_key(key)?;
111 let bytes = value.to_string().len();
112 if bytes > self.max_value_bytes {
113 return Err(format!(
114 "memory value is {bytes} bytes; memory.max_value_bytes is {}",
115 self.max_value_bytes
116 ));
117 }
118 self.probe(d);
119 let rec = Record {
120 value,
121 ts: now_ms(),
122 ttl_ms,
123 by: by.map(str::to_string),
124 };
125 d.put(
126 Kind::Memory,
127 key,
128 serde_json::to_value(&rec).unwrap_or(Value::Null),
129 None,
130 )
131 .map_err(|e| e.to_string())?;
132 if let Some(idx) = &mut self.index {
133 idx.insert(key.to_string(), rec.ts);
134 self.write_index(d).map_err(|e| e.to_string())?;
135 }
136 Ok(json!({"ok": true, "key": key, "meta": rec.meta()}))
137 }
138
139 pub fn push(
144 &mut self,
145 d: &Durable,
146 key: &str,
147 value: Value,
148 by: Option<&str>,
149 ) -> Result<Value, String> {
150 let cur = self.get(d, key)?;
151 let mut arr = if cur["found"] == json!(true) {
152 match cur["value"].as_array() {
153 Some(a) => a.clone(),
154 None => return Err(format!("memory.push: the value at {key:?} is not an array")),
155 }
156 } else {
157 Vec::new()
158 };
159 arr.push(value);
160 let length = arr.len();
161 self.set(d, key, Value::Array(arr), None, by)?;
162 Ok(json!({"ok": true, "key": key, "length": length}))
163 }
164
165 pub fn shift(&mut self, d: &Durable, key: &str, by: Option<&str>) -> Result<Value, String> {
169 self.take(d, key, by, true)
170 }
171
172 pub fn pop(&mut self, d: &Durable, key: &str, by: Option<&str>) -> Result<Value, String> {
174 self.take(d, key, by, false)
175 }
176
177 fn take(
178 &mut self,
179 d: &Durable,
180 key: &str,
181 by: Option<&str>,
182 first: bool,
183 ) -> Result<Value, String> {
184 let cur = self.get(d, key)?;
185 if cur["found"] != json!(true) {
186 return Ok(json!({"found": false, "key": key, "remaining": 0}));
187 }
188 let mut arr = match cur["value"].as_array() {
189 Some(a) => a.clone(),
190 None => {
191 return Err(format!(
192 "memory.{}: the value at {key:?} is not an array",
193 if first { "shift" } else { "pop" }
194 ));
195 }
196 };
197 if arr.is_empty() {
198 return Ok(json!({"found": false, "key": key, "remaining": 0}));
199 }
200 let value = if first {
201 arr.remove(0)
202 } else {
203 arr.pop().expect("non-empty")
204 };
205 let remaining = arr.len();
206 self.set(d, key, Value::Array(arr), None, by)?;
207 Ok(json!({"found": true, "key": key, "value": value, "remaining": remaining}))
208 }
209
210 pub fn get(&mut self, d: &Durable, key: &str) -> Result<Value, String> {
212 Self::check_key(key)?;
213 match d.get(Kind::Memory, key).map_err(|e| e.to_string())? {
214 None => Ok(json!({"found": false, "key": key})),
215 Some(env) => {
216 let rec: Record = serde_json::from_value(env.state)
217 .map_err(|e| format!("memory record {key}: {e}"))?;
218 if rec.expired(now_ms()) {
219 let _ = self.delete(d, key);
220 return Ok(json!({"found": false, "key": key, "expired": true}));
221 }
222 Ok(json!({"found": true, "key": key, "value": rec.value, "meta": rec.meta()}))
223 }
224 }
225 }
226
227 pub fn delete(&mut self, d: &Durable, key: &str) -> Result<Value, String> {
229 Self::check_key(key)?;
230 self.probe(d);
231 d.delete(Kind::Memory, key).map_err(|e| e.to_string())?;
232 if let Some(idx) = &mut self.index {
233 idx.remove(key);
234 self.write_index(d).map_err(|e| e.to_string())?;
235 }
236 Ok(json!({"ok": true, "key": key}))
237 }
238
239 pub fn list(
241 &mut self,
242 d: &Durable,
243 prefix: Option<&str>,
244 limit: Option<usize>,
245 ) -> Result<Value, String> {
246 self.probe(d);
247 let limit = limit.unwrap_or(self.list_default_limit).max(1);
248 let prefix = prefix.unwrap_or("");
249 let mut keys: Vec<Value> = match &self.index {
250 Some(idx) => idx
251 .iter()
252 .filter(|(k, _)| k.starts_with(prefix))
253 .map(|(k, ts)| json!({"key": k, "ts": ts}))
254 .collect(),
255 None => {
256 let listed = d.list(Kind::Memory).map_err(|e| e.to_string())?;
257 let mut out = Vec::new();
258 for ks in listed {
259 let Some((_, id)) = crate::store::parse_key(d.prefix(), d.instance(), &ks.key)
260 else {
261 continue;
262 };
263 if id == INDEX_ID || !id.starts_with(prefix) {
264 continue;
265 }
266 out.push(json!({"key": id, "seq": ks.seq}));
267 }
268 out
269 }
270 };
271 keys.sort_by(|a, b| a["key"].as_str().cmp(&b["key"].as_str()));
272 let truncated = keys.len() > limit;
273 keys.truncate(limit);
274 Ok(json!({"keys": keys, "truncated": truncated}))
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::state::Policy;
282 use crate::store::memory::MemoryStore;
283 use std::sync::Arc;
284
285 #[test]
286 fn set_get_list_delete_ttl_and_caps() {
287 let mem = Arc::new(MemoryStore::new());
288 let d = Durable::new(mem, "agentd", "i", Policy::default(), None);
289 let mut m = Memory::new(64, 10);
290 m.set(&d, "user/name", json!("andrii"), None, Some("root"))
291 .unwrap();
292 m.set(&d, "user/tz", json!("Europe/Kyiv"), Some(1), None)
293 .unwrap();
294 m.set(&d, "other", json!({"a": 1}), None, None).unwrap();
295 let g = m.get(&d, "user/name").unwrap();
296 assert_eq!(g["found"], json!(true));
297 assert_eq!(g["value"], json!("andrii"));
298 assert_eq!(g["meta"]["by"], json!("root"));
299 std::thread::sleep(std::time::Duration::from_millis(3));
300 let g = m.get(&d, "user/tz").unwrap();
301 assert_eq!(g["found"], json!(false), "expired: {g}");
302 assert_eq!(g["expired"], json!(true));
303 let l = m.list(&d, Some("user/"), None).unwrap();
304 assert_eq!(l["keys"].as_array().unwrap().len(), 1, "{l}");
305 assert_eq!(l["keys"][0]["key"], json!("user/name"));
306 let all = m.list(&d, None, Some(1)).unwrap();
307 assert_eq!(all["truncated"], json!(true));
308 m.delete(&d, "user/name").unwrap();
309 assert_eq!(m.get(&d, "user/name").unwrap()["found"], json!(false));
310 assert!(
312 m.set(&d, "big", json!("x".repeat(100)), None, None)
313 .is_err()
314 );
315 assert!(m.set(&d, "_reserved", json!(1), None, None).is_err());
316 assert!(m.set(&d, "has space", json!(1), None, None).is_err());
317 assert!(m.get(&d, "").is_err());
318 }
319
320 #[test]
321 fn index_record_is_kept_when_the_store_cannot_list() {
322 struct NoList(MemoryStore);
324 impl crate::store::Store for NoList {
325 fn put(
326 &self,
327 k: &str,
328 s: u64,
329 e: &Value,
330 ) -> Result<crate::store::PutOutcome, StoreError> {
331 self.0.put(k, s, e)
332 }
333 fn get(&self, k: &str, s: Option<u64>) -> Result<Option<Value>, StoreError> {
334 self.0.get(k, s)
335 }
336 fn list(&self, _p: &str) -> Result<Vec<crate::store::KeySeq>, StoreError> {
337 Err(StoreError::Unsupported("list"))
338 }
339 fn delete(&self, k: &str) -> Result<(), StoreError> {
340 self.0.delete(k)
341 }
342 fn kind(&self) -> &'static str {
343 "nolist"
344 }
345 }
346 let d = Durable::new(
347 Arc::new(NoList(MemoryStore::new())),
348 "agentd",
349 "i",
350 Policy::default(),
351 None,
352 );
353 let mut m = Memory::new(1024, 10);
354 m.set(&d, "a", json!(1), None, None).unwrap();
355 m.set(&d, "b", json!(2), None, None).unwrap();
356 let idx = d
357 .get(Kind::Memory, INDEX_ID)
358 .unwrap()
359 .expect("index record");
360 assert!(idx.state["keys"].get("a").is_some());
361 assert_eq!(
362 m.list(&d, None, None).unwrap()["keys"]
363 .as_array()
364 .unwrap()
365 .len(),
366 2
367 );
368 m.delete(&d, "a").unwrap();
369 let mut m2 = Memory::new(1024, 10);
371 let l = m2.list(&d, None, None).unwrap();
372 assert_eq!(l["keys"].as_array().unwrap().len(), 1);
373 assert_eq!(l["keys"][0]["key"], json!("b"));
374 }
375}