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