1use lru::LruCache;
8use serde_json::Value;
9use std::collections::HashMap;
10use std::num::NonZeroUsize;
11use std::sync::Arc;
12use tokio::sync::RwLock;
13
14const DEFAULT_MAX_ENTITIES_PER_VIEW: usize = 500;
15const DEFAULT_MAX_ARRAY_LENGTH: usize = 100;
16const DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE: usize = 50;
17const DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE: usize = 100;
18
19pub fn cmp_seq(a: &str, b: &str) -> std::cmp::Ordering {
23 fn parse(s: &str) -> (u64, u64) {
24 let mut parts = s.splitn(2, ':');
25 let slot = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
26 let offset = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);
27 (slot, offset)
28 }
29 parse(a).cmp(&parse(b))
30}
31
32#[derive(Debug, Clone)]
34pub struct EntityCacheConfig {
35 pub max_entities_per_view: usize,
37 pub max_array_length: usize,
39 pub initial_snapshot_batch_size: usize,
41 pub subsequent_snapshot_batch_size: usize,
43}
44
45impl Default for EntityCacheConfig {
46 fn default() -> Self {
47 Self {
48 max_entities_per_view: DEFAULT_MAX_ENTITIES_PER_VIEW,
49 max_array_length: DEFAULT_MAX_ARRAY_LENGTH,
50 initial_snapshot_batch_size: DEFAULT_INITIAL_SNAPSHOT_BATCH_SIZE,
51 subsequent_snapshot_batch_size: DEFAULT_SUBSEQUENT_SNAPSHOT_BATCH_SIZE,
52 }
53 }
54}
55
56#[derive(Clone)]
62pub struct EntityCache {
63 caches: Arc<RwLock<HashMap<String, LruCache<String, Value>>>>,
65 config: EntityCacheConfig,
66}
67
68impl EntityCache {
69 pub fn new() -> Self {
71 Self::with_config(EntityCacheConfig::default())
72 }
73
74 pub fn with_config(config: EntityCacheConfig) -> Self {
76 Self {
77 caches: Arc::new(RwLock::new(HashMap::new())),
78 config,
79 }
80 }
81
82 pub fn max_entities_per_view(&self) -> usize {
87 self.config.max_entities_per_view
88 }
89
90 pub async fn upsert(&self, view_id: &str, key: &str, patch: Value) {
91 self.upsert_with_append(view_id, key, patch, &[]).await;
92 }
93
94 pub async fn upsert_with_append(
95 &self,
96 view_id: &str,
97 key: &str,
98 patch: Value,
99 append_paths: &[String],
100 ) {
101 let mut caches = self.caches.write().await;
102
103 let cache = caches.entry(view_id.to_string()).or_insert_with(|| {
104 LruCache::new(
105 NonZeroUsize::new(self.config.max_entities_per_view)
106 .expect("max_entities_per_view must be > 0"),
107 )
108 });
109
110 let max_array_length = self.config.max_array_length;
111
112 if let Some(entity) = cache.get_mut(key) {
113 deep_merge_with_append(entity, patch, append_paths, max_array_length);
114 } else {
115 let new_entity = truncate_arrays_if_needed(patch, max_array_length);
116 cache.put(key.to_string(), new_entity);
117 }
118 }
119
120 pub async fn get_all(&self, view_id: &str) -> Vec<(String, Value)> {
125 let caches = self.caches.read().await;
126
127 caches
128 .get(view_id)
129 .map(|cache| cache.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
130 .unwrap_or_default()
131 }
132
133 pub async fn get_after(
139 &self,
140 view_id: &str,
141 cursor: &str,
142 limit: Option<usize>,
143 ) -> Vec<(String, Value)> {
144 let caches = self.caches.read().await;
145
146 if let Some(cache) = caches.get(view_id) {
147 let mut results: Vec<(String, Value)> = cache
148 .iter()
149 .filter(|(_, entity)| {
150 entity
151 .get("_seq")
152 .and_then(|s| s.as_str())
153 .map(|seq| cmp_seq(seq, cursor) == std::cmp::Ordering::Greater)
154 .unwrap_or(false)
155 })
156 .map(|(k, v)| (k.clone(), v.clone()))
157 .collect();
158
159 results.sort_by(|a, b| {
161 let seq_a = a.1.get("_seq").and_then(|s| s.as_str()).unwrap_or("");
162 let seq_b = b.1.get("_seq").and_then(|s| s.as_str()).unwrap_or("");
163 cmp_seq(seq_a, seq_b)
164 });
165
166 if let Some(limit) = limit {
168 results.truncate(limit);
169 }
170
171 results
172 } else {
173 vec![]
174 }
175 }
176
177 pub async fn get(&self, view_id: &str, key: &str) -> Option<Value> {
179 let caches = self.caches.read().await;
180 caches
181 .get(view_id)
182 .and_then(|cache| cache.peek(key).cloned())
183 }
184
185 pub async fn remove(&self, view_id: &str, key: &str) -> Option<Value> {
187 let mut caches = self.caches.write().await;
188 caches.get_mut(view_id).and_then(|cache| cache.pop(key))
189 }
190
191 pub async fn len(&self, view_id: &str) -> usize {
193 let caches = self.caches.read().await;
194 caches.get(view_id).map(|c| c.len()).unwrap_or(0)
195 }
196
197 pub async fn is_empty(&self, view_id: &str) -> bool {
199 self.len(view_id).await == 0
200 }
201
202 pub fn snapshot_config(&self) -> SnapshotBatchConfig {
204 SnapshotBatchConfig {
205 initial_batch_size: self.config.initial_snapshot_batch_size,
206 subsequent_batch_size: self.config.subsequent_snapshot_batch_size,
207 }
208 }
209
210 pub async fn clear(&self, view_id: &str) {
212 let mut caches = self.caches.write().await;
213 if let Some(cache) = caches.get_mut(view_id) {
214 cache.clear();
215 }
216 }
217
218 pub async fn clear_all(&self) {
219 let mut caches = self.caches.write().await;
220 caches.clear();
221 }
222
223 pub async fn dump(&self) -> Vec<(String, Vec<(String, Value)>)> {
228 let caches = self.caches.read().await;
229 caches
230 .iter()
231 .map(|(view_id, cache)| {
232 (
233 view_id.clone(),
234 cache
235 .iter()
236 .map(|(key, entity)| (key.clone(), entity.clone()))
237 .collect(),
238 )
239 })
240 .collect()
241 }
242
243 pub async fn hydrate(&self, views: Vec<(String, Vec<(String, Value)>)>) {
248 let mut caches = self.caches.write().await;
249 for (view_id, entries) in views {
250 let cache = caches.entry(view_id).or_insert_with(|| {
251 LruCache::new(
252 NonZeroUsize::new(self.config.max_entities_per_view)
253 .expect("max_entities_per_view must be > 0"),
254 )
255 });
256 for (key, entity) in entries.into_iter().rev() {
257 cache.put(key, entity);
258 }
259 }
260 }
261
262 pub async fn stats(&self) -> CacheStats {
263 let caches = self.caches.read().await;
264 let mut total_entities = 0;
265 let mut views = Vec::new();
266
267 for (view_id, cache) in caches.iter() {
268 let count = cache.len();
269 total_entities += count;
270 views.push((view_id.clone(), count));
271 }
272
273 views.sort_by_key(|b| std::cmp::Reverse(b.1));
274
275 CacheStats {
276 view_count: caches.len(),
277 total_entities,
278 top_views: views.into_iter().take(5).collect(),
279 }
280 }
281}
282
283#[derive(Debug, Clone, PartialEq, Eq)]
285pub struct CacheStats {
286 pub view_count: usize,
287 pub total_entities: usize,
288 pub top_views: Vec<(String, usize)>,
289}
290
291#[derive(Debug, Clone, Copy)]
292pub struct SnapshotBatchConfig {
293 pub initial_batch_size: usize,
294 pub subsequent_batch_size: usize,
295}
296
297impl Default for EntityCache {
298 fn default() -> Self {
299 Self::new()
300 }
301}
302
303fn deep_merge_with_append(
304 base: &mut Value,
305 patch: Value,
306 append_paths: &[String],
307 max_array_length: usize,
308) {
309 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
310}
311
312fn deep_merge_with_append_inner(
313 base: &mut Value,
314 patch: Value,
315 append_paths: &[String],
316 current_path: &str,
317 max_array_length: usize,
318) {
319 match (base, patch) {
320 (Value::Object(base_map), Value::Object(patch_map)) => {
321 for (key, patch_value) in patch_map {
322 let child_path = if current_path.is_empty() {
323 key.clone()
324 } else {
325 format!("{}.{}", current_path, key)
326 };
327
328 if let Some(base_value) = base_map.get_mut(&key) {
329 deep_merge_with_append_inner(
330 base_value,
331 patch_value,
332 append_paths,
333 &child_path,
334 max_array_length,
335 );
336 } else {
337 base_map.insert(
338 key,
339 truncate_arrays_if_needed(patch_value, max_array_length),
340 );
341 }
342 }
343 }
344
345 (Value::Array(base_arr), Value::Array(patch_arr)) => {
346 let should_append = append_paths.iter().any(|p| p == current_path);
347 if should_append {
348 base_arr.extend(patch_arr);
349 if base_arr.len() > max_array_length {
350 let excess = base_arr.len() - max_array_length;
351 base_arr.drain(0..excess);
352 }
353 } else {
354 *base_arr = patch_arr;
355 if base_arr.len() > max_array_length {
356 let excess = base_arr.len() - max_array_length;
357 base_arr.drain(0..excess);
358 }
359 }
360 }
361
362 (base, patch_value) => {
363 *base = truncate_arrays_if_needed(patch_value, max_array_length);
364 }
365 }
366}
367
368fn truncate_arrays_if_needed(value: Value, max_array_length: usize) -> Value {
370 match value {
371 Value::Array(mut arr) => {
372 if arr.len() > max_array_length {
374 let excess = arr.len() - max_array_length;
375 arr.drain(0..excess);
376 }
377 Value::Array(
379 arr.into_iter()
380 .map(|v| truncate_arrays_if_needed(v, max_array_length))
381 .collect(),
382 )
383 }
384 Value::Object(map) => Value::Object(
385 map.into_iter()
386 .map(|(k, v)| (k, truncate_arrays_if_needed(v, max_array_length)))
387 .collect(),
388 ),
389 other => other,
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396 use serde_json::json;
397
398 #[tokio::test]
399 async fn test_basic_upsert_and_get() {
400 let cache = EntityCache::new();
401
402 cache
403 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
404 .await;
405
406 let entity = cache.get("tokens/list", "abc123").await;
407 assert!(entity.is_some());
408 assert_eq!(entity.unwrap()["name"], "Test Token");
409 }
410
411 #[tokio::test]
412 async fn test_deep_merge_objects() {
413 let cache = EntityCache::new();
414
415 cache
416 .upsert(
417 "tokens/list",
418 "abc123",
419 json!({
420 "id": "abc123",
421 "metrics": {"volume": 100}
422 }),
423 )
424 .await;
425
426 cache
427 .upsert(
428 "tokens/list",
429 "abc123",
430 json!({
431 "metrics": {"trades": 50}
432 }),
433 )
434 .await;
435
436 let entity = cache.get("tokens/list", "abc123").await.unwrap();
437 assert_eq!(entity["id"], "abc123");
438 assert_eq!(entity["metrics"]["volume"], 100);
439 assert_eq!(entity["metrics"]["trades"], 50);
440 }
441
442 #[tokio::test]
443 async fn test_array_append() {
444 let cache = EntityCache::new();
445
446 cache
447 .upsert(
448 "tokens/list",
449 "abc123",
450 json!({
451 "events": [{"type": "buy", "amount": 100}]
452 }),
453 )
454 .await;
455
456 cache
457 .upsert_with_append(
458 "tokens/list",
459 "abc123",
460 json!({
461 "events": [{"type": "sell", "amount": 50}]
462 }),
463 &["events".to_string()],
464 )
465 .await;
466
467 let entity = cache.get("tokens/list", "abc123").await.unwrap();
468 let events = entity["events"].as_array().unwrap();
469 assert_eq!(events.len(), 2);
470 assert_eq!(events[0]["type"], "buy");
471 assert_eq!(events[1]["type"], "sell");
472 }
473
474 #[tokio::test]
475 async fn test_array_lru_eviction() {
476 let config = EntityCacheConfig {
477 max_entities_per_view: 1000,
478 max_array_length: 3,
479 ..Default::default()
480 };
481 let cache = EntityCache::with_config(config);
482
483 cache
484 .upsert(
485 "tokens/list",
486 "abc123",
487 json!({
488 "events": [
489 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
490 ]
491 }),
492 )
493 .await;
494
495 let entity = cache.get("tokens/list", "abc123").await.unwrap();
496 let events = entity["events"].as_array().unwrap();
497
498 assert_eq!(events.len(), 3);
499 assert_eq!(events[0]["id"], 3);
500 assert_eq!(events[1]["id"], 4);
501 assert_eq!(events[2]["id"], 5);
502 }
503
504 #[tokio::test]
505 async fn test_array_append_with_lru() {
506 let config = EntityCacheConfig {
507 max_entities_per_view: 1000,
508 max_array_length: 3,
509 ..Default::default()
510 };
511 let cache = EntityCache::with_config(config);
512
513 cache
514 .upsert(
515 "tokens/list",
516 "abc123",
517 json!({
518 "events": [{"id": 1}, {"id": 2}]
519 }),
520 )
521 .await;
522
523 cache
524 .upsert_with_append(
525 "tokens/list",
526 "abc123",
527 json!({
528 "events": [{"id": 3}, {"id": 4}]
529 }),
530 &["events".to_string()],
531 )
532 .await;
533
534 let entity = cache.get("tokens/list", "abc123").await.unwrap();
535 let events = entity["events"].as_array().unwrap();
536
537 assert_eq!(events.len(), 3);
539 assert_eq!(events[0]["id"], 2);
540 assert_eq!(events[1]["id"], 3);
541 assert_eq!(events[2]["id"], 4);
542 }
543
544 #[tokio::test]
545 async fn test_entity_lru_eviction() {
546 let config = EntityCacheConfig {
547 max_entities_per_view: 2,
548 max_array_length: 100,
549 ..Default::default()
550 };
551 let cache = EntityCache::with_config(config);
552
553 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
554 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
555 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
556
557 assert!(cache.get("tokens/list", "key1").await.is_none());
558 assert!(cache.get("tokens/list", "key2").await.is_some());
559 assert!(cache.get("tokens/list", "key3").await.is_some());
560 }
561
562 #[tokio::test]
563 async fn test_get_all() {
564 let cache = EntityCache::new();
565
566 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
567 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
568
569 let all = cache.get_all("tokens/list").await;
570 assert_eq!(all.len(), 2);
571 }
572
573 #[tokio::test]
574 async fn remove_is_scoped_to_one_entity() {
575 let cache = EntityCache::new();
576 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
577 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
578
579 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
580 assert!(cache.get("tokens/list", "one").await.is_none());
581 assert!(cache.get("tokens/list", "two").await.is_some());
582 }
583
584 #[tokio::test]
585 async fn test_separate_views() {
586 let cache = EntityCache::new();
587
588 cache
589 .upsert("tokens/list", "key1", json!({"type": "token"}))
590 .await;
591 cache
592 .upsert("games/list", "key1", json!({"type": "game"}))
593 .await;
594
595 let token = cache.get("tokens/list", "key1").await.unwrap();
596 let game = cache.get("games/list", "key1").await.unwrap();
597
598 assert_eq!(token["type"], "token");
599 assert_eq!(game["type"], "game");
600 }
601
602 #[test]
603 fn test_deep_merge_with_append() {
604 let mut base = json!({
605 "a": 1,
606 "b": {"c": 2},
607 "arr": [1, 2]
608 });
609
610 let patch = json!({
611 "b": {"d": 3},
612 "arr": [3],
613 "e": 4
614 });
615
616 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
617
618 assert_eq!(base["a"], 1);
619 assert_eq!(base["b"]["c"], 2);
620 assert_eq!(base["b"]["d"], 3);
621 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
622 assert_eq!(base["e"], 4);
623 }
624
625 #[test]
626 fn test_deep_merge_replace_array() {
627 let mut base = json!({
628 "arr": [1, 2, 3]
629 });
630
631 let patch = json!({
632 "arr": [4, 5]
633 });
634
635 deep_merge_with_append(&mut base, patch, &[], 100);
636
637 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
638 assert_eq!(base["arr"][0], 4);
639 assert_eq!(base["arr"][1], 5);
640 }
641
642 #[test]
643 fn test_deep_merge_nested_append() {
644 let mut base = json!({
645 "stats": {"events": [1, 2]}
646 });
647
648 let patch = json!({
649 "stats": {"events": [3]}
650 });
651
652 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
653
654 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
655 }
656
657 #[test]
658 fn test_snapshot_config_defaults() {
659 let cache = EntityCache::new();
660 let config = cache.snapshot_config();
661
662 assert_eq!(config.initial_batch_size, 50);
663 assert_eq!(config.subsequent_batch_size, 100);
664 }
665
666 #[test]
667 fn test_snapshot_config_custom() {
668 let config = EntityCacheConfig {
669 initial_snapshot_batch_size: 25,
670 subsequent_snapshot_batch_size: 75,
671 ..Default::default()
672 };
673 let cache = EntityCache::with_config(config);
674 let snapshot_config = cache.snapshot_config();
675
676 assert_eq!(snapshot_config.initial_batch_size, 25);
677 assert_eq!(snapshot_config.subsequent_batch_size, 75);
678 }
679
680 #[tokio::test]
681 async fn test_get_after() {
682 let cache = EntityCache::new();
683
684 cache
686 .upsert(
687 "tokens/list",
688 "key1",
689 json!({"id": 1, "_seq": "100:000000000001"}),
690 )
691 .await;
692 cache
693 .upsert(
694 "tokens/list",
695 "key2",
696 json!({"id": 2, "_seq": "100:000000000002"}),
697 )
698 .await;
699 cache
700 .upsert(
701 "tokens/list",
702 "key3",
703 json!({"id": 3, "_seq": "100:000000000003"}),
704 )
705 .await;
706 cache
707 .upsert(
708 "tokens/list",
709 "key4",
710 json!({"id": 4, "_seq": "101:000000000001"}),
711 )
712 .await;
713
714 let after = cache
716 .get_after("tokens/list", "100:000000000002", None)
717 .await;
718
719 assert_eq!(after.len(), 2);
721 assert_eq!(after[0].0, "key3");
722 assert_eq!(after[1].0, "key4");
723 }
724
725 #[tokio::test]
726 async fn test_get_after_with_limit() {
727 let cache = EntityCache::new();
728
729 cache
731 .upsert(
732 "tokens/list",
733 "key1",
734 json!({"id": 1, "_seq": "100:000000000001"}),
735 )
736 .await;
737 cache
738 .upsert(
739 "tokens/list",
740 "key2",
741 json!({"id": 2, "_seq": "100:000000000002"}),
742 )
743 .await;
744 cache
745 .upsert(
746 "tokens/list",
747 "key3",
748 json!({"id": 3, "_seq": "100:000000000003"}),
749 )
750 .await;
751
752 let after = cache
754 .get_after("tokens/list", "100:000000000000", Some(2))
755 .await;
756
757 assert_eq!(after.len(), 2);
759 assert_eq!(after[0].0, "key1");
760 assert_eq!(after[1].0, "key2");
761 }
762
763 #[tokio::test]
764 async fn test_get_after_empty_result() {
765 let cache = EntityCache::new();
766
767 cache
768 .upsert(
769 "tokens/list",
770 "key1",
771 json!({"id": 1, "_seq": "100:000000000001"}),
772 )
773 .await;
774
775 let after = cache
777 .get_after("tokens/list", "999:000000000000", None)
778 .await;
779
780 assert!(after.is_empty());
781 }
782
783 #[tokio::test]
784 async fn test_get_after_missing_seq() {
785 let cache = EntityCache::new();
786
787 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
789
790 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
792
793 assert!(after.is_empty());
794 }
795}