1use std::{
4 collections::{BTreeMap, BTreeSet, HashMap},
5 ops::{Bound, ControlFlow},
6 sync::{Arc, PoisonError, RwLock},
7 time::{Duration, SystemTime, UNIX_EPOCH},
8};
9
10use hidane_proto::ordered::Fields;
11
12use super::{
13 Change, Commit, ListItem, ListOptions, ReadTime, Store, StoreError, StoredDocument, Visit,
14 WriteBatch,
15};
16use crate::{
17 key::{encode_escaped, encode_path, encode_path_prefix, path_subtree_end},
18 path::ResourcePath,
19};
20
21type Clock = Box<dyn Fn() -> i64 + Send + Sync>;
22
23pub const DEFAULT_RETENTION: Duration = Duration::from_secs(60 * 60);
25
26pub struct MemoryStore {
27 databases: RwLock<HashMap<String, Arc<RwLock<Database>>>>,
28 clock: Clock,
29 retention_micros: i64,
30}
31
32#[derive(Default)]
33struct Database {
34 last_commit: i64,
36 documents: BTreeMap<Vec<u8>, Record>,
38 groups: BTreeSet<Vec<u8>>,
41}
42
43impl Database {
44 fn subtree_has_live(&self, prefix: &[u8], time: i64) -> bool {
46 self.documents
47 .range(prefix.to_vec()..path_subtree_end(prefix))
48 .any(|(_, record)| record.at(time).is_some())
49 }
50}
51
52struct Record {
53 path: Arc<ResourcePath>,
54 versions: Vec<(i64, Option<Arc<StoredDocument>>)>,
56}
57
58impl Record {
59 fn at(&self, time: i64) -> Option<&Arc<StoredDocument>> {
60 let newer = self.versions.partition_point(|(t, _)| *t <= time);
61 newer
62 .checked_sub(1)
63 .and_then(|i| self.versions[i].1.as_ref())
64 }
65
66 fn latest(&self) -> Option<&Arc<StoredDocument>> {
67 self.versions.last().and_then(|(_, doc)| doc.as_ref())
68 }
69
70 fn deleted_at(&self) -> Option<i64> {
72 match self.versions.last() {
73 Some((time, None)) => Some(*time),
74 _ => None,
75 }
76 }
77
78 fn prune(&mut self, cutoff: i64) {
81 let reachable_from = self.versions.partition_point(|(t, _)| *t <= cutoff);
82 if reachable_from > 1 {
83 self.versions.drain(..reachable_from - 1);
84 }
85 }
86}
87
88impl Default for MemoryStore {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl MemoryStore {
95 pub fn new() -> Self {
96 Self::with_clock(system_micros)
97 }
98
99 pub fn with_clock(clock: impl Fn() -> i64 + Send + Sync + 'static) -> Self {
102 Self {
103 databases: RwLock::default(),
104 clock: Box::new(clock),
105 retention_micros: micros(DEFAULT_RETENTION),
106 }
107 }
108
109 #[must_use]
111 pub fn with_retention(mut self, retention: Duration) -> Self {
112 self.retention_micros = micros(retention);
113 self
114 }
115
116 fn existing(&self, database: &str) -> Option<Arc<RwLock<Database>>> {
117 self.databases
118 .read()
119 .unwrap_or_else(PoisonError::into_inner)
120 .get(database)
121 .cloned()
122 }
123
124 fn get_or_create(&self, database: &str) -> Arc<RwLock<Database>> {
125 if let Some(db) = self.existing(database) {
126 return db;
127 }
128 self.databases
129 .write()
130 .unwrap_or_else(PoisonError::into_inner)
131 .entry(database.to_owned())
132 .or_default()
133 .clone()
134 }
135
136 fn read<R>(&self, database: &str, empty: R, f: impl FnOnce(&Database) -> R) -> R {
137 match self.existing(database) {
138 Some(db) => f(&db.read().unwrap_or_else(PoisonError::into_inner)),
139 None => empty,
140 }
141 }
142}
143
144fn system_micros() -> i64 {
145 SystemTime::now()
146 .duration_since(UNIX_EPOCH)
147 .map_or(0, |d| i64::try_from(d.as_micros()).unwrap_or(i64::MAX))
148}
149
150fn micros(d: Duration) -> i64 {
151 i64::try_from(d.as_micros()).unwrap_or(i64::MAX)
152}
153
154impl Store for MemoryStore {
155 fn latest_read_time(&self, database: &str) -> ReadTime {
156 let last = self.read(database, 0, |db| db.last_commit);
157 ReadTime((self.clock)().max(last))
158 }
159
160 fn earliest_read_time(&self, database: &str) -> ReadTime {
161 ReadTime(
162 self.latest_read_time(database)
163 .0
164 .saturating_sub(self.retention_micros),
165 )
166 }
167
168 fn get(
169 &self,
170 database: &str,
171 path: &ResourcePath,
172 at: ReadTime,
173 ) -> Option<Arc<StoredDocument>> {
174 self.read(database, None, |db| {
175 db.documents
176 .get(&encode_path(path.segments()))
177 .and_then(|record| record.at(at.0))
178 .cloned()
179 })
180 }
181
182 fn list_collection(
183 &self,
184 database: &str,
185 collection: &ResourcePath,
186 at: ReadTime,
187 options: ListOptions<'_>,
188 visit: &mut dyn FnMut(ListItem<'_>) -> ControlFlow<()>,
189 ) {
190 self.read(database, (), |db| {
191 let prefix = encode_path_prefix(collection.segments());
192 let end = Bound::Excluded(path_subtree_end(&prefix));
193 let depth = collection.len() + 1;
194 let mut start = match options.after {
195 Some(after) => {
196 Bound::Included(path_subtree_end(&encode_path_prefix(after.segments())))
197 }
198 None => Bound::Included(prefix),
199 };
200 'scan: loop {
201 let Some((_, record)) = db.documents.range((start.clone(), end.clone())).next()
203 else {
204 return;
205 };
206 let child = encode_path_prefix(record.path.segments().take(depth));
207 let live = (record.path.len() == depth)
208 .then(|| record.at(at.0))
209 .flatten();
210 let flow = match live {
211 Some(doc) => visit(ListItem::Document(doc)),
212 None if options.include_missing && db.subtree_has_live(&child, at.0) => {
213 let path = ResourcePath::from_segments(record.path.segments().take(depth));
214 visit(ListItem::Missing(&path))
215 }
216 None => ControlFlow::Continue(()),
217 };
218 if flow.is_break() {
219 return;
220 }
221 start = Bound::Included(path_subtree_end(&child));
222 continue 'scan;
223 }
224 });
225 }
226
227 fn scan_collection_group(
228 &self,
229 database: &str,
230 parent: &ResourcePath,
231 collection_id: &str,
232 at: ReadTime,
233 visit: &mut Visit<'_>,
234 ) {
235 self.read(database, (), |db| {
236 let mut prefix = encode_escaped(collection_id.as_bytes());
237 let group_len = prefix.len();
238 prefix.extend(encode_path_prefix(parent.segments()));
239 let end = path_subtree_end(&prefix);
240 for entry in db.groups.range(prefix..end) {
241 let doc = db
242 .documents
243 .get(&entry[group_len..])
244 .filter(|record| record.path.len() > parent.len())
246 .and_then(|record| record.at(at.0));
247 if let Some(doc) = doc
248 && visit(doc).is_break()
249 {
250 return;
251 }
252 }
253 });
254 }
255
256 fn scan_descendants(
257 &self,
258 database: &str,
259 parent: &ResourcePath,
260 at: ReadTime,
261 visit: &mut Visit<'_>,
262 ) {
263 self.read(database, (), |db| {
264 let start = Bound::Excluded(encode_path(parent.segments()));
265 let end = Bound::Excluded(path_subtree_end(&encode_path_prefix(parent.segments())));
266 for (_, record) in db.documents.range((start, end)) {
267 if let Some(doc) = record.at(at.0)
268 && visit(doc).is_break()
269 {
270 return;
271 }
272 }
273 });
274 }
275
276 fn list_collection_ids(
277 &self,
278 database: &str,
279 parent: &ResourcePath,
280 at: ReadTime,
281 ) -> Vec<String> {
282 self.read(database, Vec::new(), |db| {
283 let prefix = encode_path_prefix(parent.segments());
284 let end = Bound::Excluded(path_subtree_end(&prefix));
285 let mut start = Bound::Excluded(encode_path(parent.segments()));
286 let mut ids = Vec::new();
287 'scan: loop {
288 for (_, record) in db.documents.range((start.clone(), end.clone())) {
289 if record.at(at.0).is_none() {
290 continue;
291 }
292 let collection = record.path.segments().take(parent.len() + 1);
295 if let Some(id) = collection.clone().last() {
296 ids.push(id.to_owned());
297 }
298 let collection_prefix = encode_path_prefix(collection);
299 start = Bound::Included(path_subtree_end(&collection_prefix));
300 continue 'scan;
301 }
302 return ids;
303 }
304 })
305 }
306
307 fn commit(
308 &self,
309 database: &str,
310 write: &mut dyn FnMut(&mut dyn WriteBatch) -> Result<(), StoreError>,
311 ) -> Result<Commit, StoreError> {
312 let db = self.get_or_create(database);
313 let mut db = db.write().unwrap_or_else(PoisonError::into_inner);
314 let commit_time = (self.clock)().max(db.last_commit + 1);
315
316 let mut batch = MemoryBatch {
317 db: &db,
318 commit_time,
319 pending: BTreeMap::new(),
320 };
321 write(&mut batch)?;
322 let pending = batch.pending;
323
324 let cutoff = commit_time.saturating_sub(self.retention_micros);
325 let mut changes = Vec::with_capacity(pending.len());
326 for (key, (path, after)) in pending {
327 let before = db.documents.get(&key).and_then(Record::latest).cloned();
328 if before.is_none() && after.is_none() {
329 continue; }
331 if !db.documents.contains_key(&key)
332 && let Some(collection_id) = path.collection_id()
333 {
334 let mut group_key = encode_escaped(collection_id.as_bytes());
335 group_key.extend_from_slice(&key);
336 db.groups.insert(group_key);
337 }
338 let record = db.documents.entry(key).or_insert_with(|| Record {
339 path: Arc::clone(&path),
340 versions: Vec::with_capacity(1),
342 });
343 record.versions.push((commit_time, after.clone()));
344 record.prune(cutoff);
345 changes.push(Change {
346 path,
347 before,
348 after,
349 });
350 }
351 db.last_commit = commit_time;
352 Ok(Commit {
353 commit_time: ReadTime(commit_time),
354 changes,
355 })
356 }
357
358 fn clear_database(&self, database: &str) -> ReadTime {
359 let db = self.get_or_create(database);
360 let mut db = db.write().unwrap_or_else(PoisonError::into_inner);
361 let at = (self.clock)().max(db.last_commit + 1);
362 db.documents.clear();
363 db.groups.clear();
364 db.last_commit = at;
365 ReadTime(at)
366 }
367
368 fn database_names(&self) -> Vec<String> {
369 self.databases
370 .read()
371 .unwrap_or_else(PoisonError::into_inner)
372 .keys()
373 .cloned()
374 .collect()
375 }
376
377 fn clear(&self) {
378 self.databases
379 .write()
380 .unwrap_or_else(PoisonError::into_inner)
381 .clear();
382 }
383}
384
385type PendingWrite = (Arc<ResourcePath>, Option<Arc<StoredDocument>>);
387
388struct MemoryBatch<'a> {
389 db: &'a Database,
390 commit_time: i64,
391 pending: BTreeMap<Vec<u8>, PendingWrite>,
393}
394
395impl MemoryBatch<'_> {
396 fn shared_path(&self, key: &[u8], path: &ResourcePath) -> Arc<ResourcePath> {
398 self.pending
399 .get(key)
400 .map(|(p, _)| Arc::clone(p))
401 .or_else(|| self.db.documents.get(key).map(|r| Arc::clone(&r.path)))
402 .unwrap_or_else(|| Arc::new(path.clone()))
403 }
404}
405
406impl WriteBatch for MemoryBatch<'_> {
407 fn commit_time(&self) -> ReadTime {
408 ReadTime(self.commit_time)
409 }
410
411 fn get(&self, path: &ResourcePath) -> Option<Arc<StoredDocument>> {
412 let key = encode_path(path.segments());
413 match self.pending.get(&key) {
414 Some((_, pending)) => pending.clone(),
415 None => self
416 .db
417 .documents
418 .get(&key)
419 .and_then(Record::latest)
420 .cloned(),
421 }
422 }
423
424 fn set(&mut self, path: &ResourcePath, fields: Fields) {
425 let now = ReadTime(self.commit_time).to_timestamp();
426 let create_time = self
427 .get(path)
428 .map_or_else(|| now, |existing| existing.create_time);
429 let key = encode_path(path.segments());
430 let shared = self.shared_path(&key, path);
431 let document = StoredDocument::new(Arc::clone(&shared), fields, create_time, now);
432 self.pending.insert(key, (shared, Some(Arc::new(document))));
433 }
434
435 fn delete(&mut self, path: &ResourcePath) {
436 let key = encode_path(path.segments());
437 let shared = self.shared_path(&key, path);
438 self.pending.insert(key, (shared, None));
439 }
440
441 fn revert(&mut self, path: &ResourcePath) {
442 self.pending.remove(&encode_path(path.segments()));
443 }
444
445 fn deleted_at(&self, path: &ResourcePath) -> Option<ReadTime> {
446 let key = encode_path(path.segments());
447 let record = self.db.documents.get(&key);
448 let committed = || record.and_then(Record::deleted_at).map(ReadTime);
449 match self.pending.get(&key) {
450 Some((_, Some(_))) => None,
451 Some((_, None)) if record.and_then(Record::latest).is_some() => {
453 Some(ReadTime(self.commit_time))
454 }
455 Some((_, None)) | None => committed(),
456 }
457 }
458}
459
460#[cfg(test)]
461mod tests;