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 stats(&self) -> CacheStats {
216 let caches = self.caches.read().await;
217 let mut total_entities = 0;
218 let mut views = Vec::new();
219
220 for (view_id, cache) in caches.iter() {
221 let count = cache.len();
222 total_entities += count;
223 views.push((view_id.clone(), count));
224 }
225
226 views.sort_by_key(|b| std::cmp::Reverse(b.1));
227
228 CacheStats {
229 view_count: caches.len(),
230 total_entities,
231 top_views: views.into_iter().take(5).collect(),
232 }
233 }
234}
235
236#[derive(Debug)]
237pub struct CacheStats {
238 pub view_count: usize,
239 pub total_entities: usize,
240 pub top_views: Vec<(String, usize)>,
241}
242
243#[derive(Debug, Clone, Copy)]
244pub struct SnapshotBatchConfig {
245 pub initial_batch_size: usize,
246 pub subsequent_batch_size: usize,
247}
248
249impl Default for EntityCache {
250 fn default() -> Self {
251 Self::new()
252 }
253}
254
255fn deep_merge_with_append(
256 base: &mut Value,
257 patch: Value,
258 append_paths: &[String],
259 max_array_length: usize,
260) {
261 deep_merge_with_append_inner(base, patch, append_paths, "", max_array_length);
262}
263
264fn deep_merge_with_append_inner(
265 base: &mut Value,
266 patch: Value,
267 append_paths: &[String],
268 current_path: &str,
269 max_array_length: usize,
270) {
271 match (base, patch) {
272 (Value::Object(base_map), Value::Object(patch_map)) => {
273 for (key, patch_value) in patch_map {
274 let child_path = if current_path.is_empty() {
275 key.clone()
276 } else {
277 format!("{}.{}", current_path, key)
278 };
279
280 if let Some(base_value) = base_map.get_mut(&key) {
281 deep_merge_with_append_inner(
282 base_value,
283 patch_value,
284 append_paths,
285 &child_path,
286 max_array_length,
287 );
288 } else {
289 base_map.insert(
290 key,
291 truncate_arrays_if_needed(patch_value, max_array_length),
292 );
293 }
294 }
295 }
296
297 (Value::Array(base_arr), Value::Array(patch_arr)) => {
298 let should_append = append_paths.iter().any(|p| p == current_path);
299 if should_append {
300 base_arr.extend(patch_arr);
301 if base_arr.len() > max_array_length {
302 let excess = base_arr.len() - max_array_length;
303 base_arr.drain(0..excess);
304 }
305 } else {
306 *base_arr = patch_arr;
307 if base_arr.len() > max_array_length {
308 let excess = base_arr.len() - max_array_length;
309 base_arr.drain(0..excess);
310 }
311 }
312 }
313
314 (base, patch_value) => {
315 *base = truncate_arrays_if_needed(patch_value, max_array_length);
316 }
317 }
318}
319
320fn truncate_arrays_if_needed(value: Value, max_array_length: usize) -> Value {
322 match value {
323 Value::Array(mut arr) => {
324 if arr.len() > max_array_length {
326 let excess = arr.len() - max_array_length;
327 arr.drain(0..excess);
328 }
329 Value::Array(
331 arr.into_iter()
332 .map(|v| truncate_arrays_if_needed(v, max_array_length))
333 .collect(),
334 )
335 }
336 Value::Object(map) => Value::Object(
337 map.into_iter()
338 .map(|(k, v)| (k, truncate_arrays_if_needed(v, max_array_length)))
339 .collect(),
340 ),
341 other => other,
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348 use serde_json::json;
349
350 #[tokio::test]
351 async fn test_basic_upsert_and_get() {
352 let cache = EntityCache::new();
353
354 cache
355 .upsert("tokens/list", "abc123", json!({"name": "Test Token"}))
356 .await;
357
358 let entity = cache.get("tokens/list", "abc123").await;
359 assert!(entity.is_some());
360 assert_eq!(entity.unwrap()["name"], "Test Token");
361 }
362
363 #[tokio::test]
364 async fn test_deep_merge_objects() {
365 let cache = EntityCache::new();
366
367 cache
368 .upsert(
369 "tokens/list",
370 "abc123",
371 json!({
372 "id": "abc123",
373 "metrics": {"volume": 100}
374 }),
375 )
376 .await;
377
378 cache
379 .upsert(
380 "tokens/list",
381 "abc123",
382 json!({
383 "metrics": {"trades": 50}
384 }),
385 )
386 .await;
387
388 let entity = cache.get("tokens/list", "abc123").await.unwrap();
389 assert_eq!(entity["id"], "abc123");
390 assert_eq!(entity["metrics"]["volume"], 100);
391 assert_eq!(entity["metrics"]["trades"], 50);
392 }
393
394 #[tokio::test]
395 async fn test_array_append() {
396 let cache = EntityCache::new();
397
398 cache
399 .upsert(
400 "tokens/list",
401 "abc123",
402 json!({
403 "events": [{"type": "buy", "amount": 100}]
404 }),
405 )
406 .await;
407
408 cache
409 .upsert_with_append(
410 "tokens/list",
411 "abc123",
412 json!({
413 "events": [{"type": "sell", "amount": 50}]
414 }),
415 &["events".to_string()],
416 )
417 .await;
418
419 let entity = cache.get("tokens/list", "abc123").await.unwrap();
420 let events = entity["events"].as_array().unwrap();
421 assert_eq!(events.len(), 2);
422 assert_eq!(events[0]["type"], "buy");
423 assert_eq!(events[1]["type"], "sell");
424 }
425
426 #[tokio::test]
427 async fn test_array_lru_eviction() {
428 let config = EntityCacheConfig {
429 max_entities_per_view: 1000,
430 max_array_length: 3,
431 ..Default::default()
432 };
433 let cache = EntityCache::with_config(config);
434
435 cache
436 .upsert(
437 "tokens/list",
438 "abc123",
439 json!({
440 "events": [
441 {"id": 1}, {"id": 2}, {"id": 3}, {"id": 4}, {"id": 5}
442 ]
443 }),
444 )
445 .await;
446
447 let entity = cache.get("tokens/list", "abc123").await.unwrap();
448 let events = entity["events"].as_array().unwrap();
449
450 assert_eq!(events.len(), 3);
451 assert_eq!(events[0]["id"], 3);
452 assert_eq!(events[1]["id"], 4);
453 assert_eq!(events[2]["id"], 5);
454 }
455
456 #[tokio::test]
457 async fn test_array_append_with_lru() {
458 let config = EntityCacheConfig {
459 max_entities_per_view: 1000,
460 max_array_length: 3,
461 ..Default::default()
462 };
463 let cache = EntityCache::with_config(config);
464
465 cache
466 .upsert(
467 "tokens/list",
468 "abc123",
469 json!({
470 "events": [{"id": 1}, {"id": 2}]
471 }),
472 )
473 .await;
474
475 cache
476 .upsert_with_append(
477 "tokens/list",
478 "abc123",
479 json!({
480 "events": [{"id": 3}, {"id": 4}]
481 }),
482 &["events".to_string()],
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);
491 assert_eq!(events[0]["id"], 2);
492 assert_eq!(events[1]["id"], 3);
493 assert_eq!(events[2]["id"], 4);
494 }
495
496 #[tokio::test]
497 async fn test_entity_lru_eviction() {
498 let config = EntityCacheConfig {
499 max_entities_per_view: 2,
500 max_array_length: 100,
501 ..Default::default()
502 };
503 let cache = EntityCache::with_config(config);
504
505 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
506 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
507 cache.upsert("tokens/list", "key3", json!({"id": 3})).await;
508
509 assert!(cache.get("tokens/list", "key1").await.is_none());
510 assert!(cache.get("tokens/list", "key2").await.is_some());
511 assert!(cache.get("tokens/list", "key3").await.is_some());
512 }
513
514 #[tokio::test]
515 async fn test_get_all() {
516 let cache = EntityCache::new();
517
518 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
519 cache.upsert("tokens/list", "key2", json!({"id": 2})).await;
520
521 let all = cache.get_all("tokens/list").await;
522 assert_eq!(all.len(), 2);
523 }
524
525 #[tokio::test]
526 async fn remove_is_scoped_to_one_entity() {
527 let cache = EntityCache::new();
528 cache.upsert("tokens/list", "one", json!({"id": 1})).await;
529 cache.upsert("tokens/list", "two", json!({"id": 2})).await;
530
531 assert_eq!(cache.remove("tokens/list", "one").await.unwrap()["id"], 1);
532 assert!(cache.get("tokens/list", "one").await.is_none());
533 assert!(cache.get("tokens/list", "two").await.is_some());
534 }
535
536 #[tokio::test]
537 async fn test_separate_views() {
538 let cache = EntityCache::new();
539
540 cache
541 .upsert("tokens/list", "key1", json!({"type": "token"}))
542 .await;
543 cache
544 .upsert("games/list", "key1", json!({"type": "game"}))
545 .await;
546
547 let token = cache.get("tokens/list", "key1").await.unwrap();
548 let game = cache.get("games/list", "key1").await.unwrap();
549
550 assert_eq!(token["type"], "token");
551 assert_eq!(game["type"], "game");
552 }
553
554 #[test]
555 fn test_deep_merge_with_append() {
556 let mut base = json!({
557 "a": 1,
558 "b": {"c": 2},
559 "arr": [1, 2]
560 });
561
562 let patch = json!({
563 "b": {"d": 3},
564 "arr": [3],
565 "e": 4
566 });
567
568 deep_merge_with_append(&mut base, patch, &["arr".to_string()], 100);
569
570 assert_eq!(base["a"], 1);
571 assert_eq!(base["b"]["c"], 2);
572 assert_eq!(base["b"]["d"], 3);
573 assert_eq!(base["arr"].as_array().unwrap().len(), 3);
574 assert_eq!(base["e"], 4);
575 }
576
577 #[test]
578 fn test_deep_merge_replace_array() {
579 let mut base = json!({
580 "arr": [1, 2, 3]
581 });
582
583 let patch = json!({
584 "arr": [4, 5]
585 });
586
587 deep_merge_with_append(&mut base, patch, &[], 100);
588
589 assert_eq!(base["arr"].as_array().unwrap().len(), 2);
590 assert_eq!(base["arr"][0], 4);
591 assert_eq!(base["arr"][1], 5);
592 }
593
594 #[test]
595 fn test_deep_merge_nested_append() {
596 let mut base = json!({
597 "stats": {"events": [1, 2]}
598 });
599
600 let patch = json!({
601 "stats": {"events": [3]}
602 });
603
604 deep_merge_with_append(&mut base, patch, &["stats.events".to_string()], 100);
605
606 assert_eq!(base["stats"]["events"].as_array().unwrap().len(), 3);
607 }
608
609 #[test]
610 fn test_snapshot_config_defaults() {
611 let cache = EntityCache::new();
612 let config = cache.snapshot_config();
613
614 assert_eq!(config.initial_batch_size, 50);
615 assert_eq!(config.subsequent_batch_size, 100);
616 }
617
618 #[test]
619 fn test_snapshot_config_custom() {
620 let config = EntityCacheConfig {
621 initial_snapshot_batch_size: 25,
622 subsequent_snapshot_batch_size: 75,
623 ..Default::default()
624 };
625 let cache = EntityCache::with_config(config);
626 let snapshot_config = cache.snapshot_config();
627
628 assert_eq!(snapshot_config.initial_batch_size, 25);
629 assert_eq!(snapshot_config.subsequent_batch_size, 75);
630 }
631
632 #[tokio::test]
633 async fn test_get_after() {
634 let cache = EntityCache::new();
635
636 cache
638 .upsert(
639 "tokens/list",
640 "key1",
641 json!({"id": 1, "_seq": "100:000000000001"}),
642 )
643 .await;
644 cache
645 .upsert(
646 "tokens/list",
647 "key2",
648 json!({"id": 2, "_seq": "100:000000000002"}),
649 )
650 .await;
651 cache
652 .upsert(
653 "tokens/list",
654 "key3",
655 json!({"id": 3, "_seq": "100:000000000003"}),
656 )
657 .await;
658 cache
659 .upsert(
660 "tokens/list",
661 "key4",
662 json!({"id": 4, "_seq": "101:000000000001"}),
663 )
664 .await;
665
666 let after = cache
668 .get_after("tokens/list", "100:000000000002", None)
669 .await;
670
671 assert_eq!(after.len(), 2);
673 assert_eq!(after[0].0, "key3");
674 assert_eq!(after[1].0, "key4");
675 }
676
677 #[tokio::test]
678 async fn test_get_after_with_limit() {
679 let cache = EntityCache::new();
680
681 cache
683 .upsert(
684 "tokens/list",
685 "key1",
686 json!({"id": 1, "_seq": "100:000000000001"}),
687 )
688 .await;
689 cache
690 .upsert(
691 "tokens/list",
692 "key2",
693 json!({"id": 2, "_seq": "100:000000000002"}),
694 )
695 .await;
696 cache
697 .upsert(
698 "tokens/list",
699 "key3",
700 json!({"id": 3, "_seq": "100:000000000003"}),
701 )
702 .await;
703
704 let after = cache
706 .get_after("tokens/list", "100:000000000000", Some(2))
707 .await;
708
709 assert_eq!(after.len(), 2);
711 assert_eq!(after[0].0, "key1");
712 assert_eq!(after[1].0, "key2");
713 }
714
715 #[tokio::test]
716 async fn test_get_after_empty_result() {
717 let cache = EntityCache::new();
718
719 cache
720 .upsert(
721 "tokens/list",
722 "key1",
723 json!({"id": 1, "_seq": "100:000000000001"}),
724 )
725 .await;
726
727 let after = cache
729 .get_after("tokens/list", "999:000000000000", None)
730 .await;
731
732 assert!(after.is_empty());
733 }
734
735 #[tokio::test]
736 async fn test_get_after_missing_seq() {
737 let cache = EntityCache::new();
738
739 cache.upsert("tokens/list", "key1", json!({"id": 1})).await;
741
742 let after = cache.get_after("tokens/list", "0:000000000000", None).await;
744
745 assert!(after.is_empty());
746 }
747}