|
1 | 1 | use super::model::DiscoveredPool; |
2 | 2 | use std::{ |
3 | | - collections::HashMap, |
| 3 | + collections::{HashMap, HashSet}, |
4 | 4 | sync::{Arc, Mutex}, |
5 | 5 | }; |
6 | 6 |
|
| 7 | +#[derive(Debug, Clone, Default)] |
| 8 | +struct PoolDiscovery { |
| 9 | + pools: Vec<DiscoveredPool>, |
| 10 | + explored_ticks: Vec<u32>, |
| 11 | +} |
| 12 | + |
7 | 13 | #[derive(Debug, Clone, Default)] |
8 | 14 | pub(super) struct PoolCache { |
9 | | - inner: Arc<Mutex<HashMap<(String, String), Vec<DiscoveredPool>>>>, |
| 15 | + pools: Arc<Mutex<HashMap<(String, String), PoolDiscovery>>>, |
| 16 | + routes: Arc<Mutex<HashMap<(String, String), Vec<DiscoveredPool>>>>, |
10 | 17 | } |
11 | 18 |
|
12 | 19 | impl PoolCache { |
13 | | - pub fn get(&self, from: &str, to: &str) -> Option<Vec<DiscoveredPool>> { |
14 | | - let cache = self.inner.lock().ok()?; |
15 | | - cache.get(&Self::key(from, to)).cloned() |
| 20 | + pub fn get(&self, from: &str, to: &str) -> Option<(Vec<DiscoveredPool>, Vec<u32>)> { |
| 21 | + let cache = self.pools.lock().ok()?; |
| 22 | + cache.get(&Self::pool_key(from, to)).map(|d| (d.pools.clone(), d.explored_ticks.clone())) |
| 23 | + } |
| 24 | + |
| 25 | + pub fn put(&self, from: &str, to: &str, pools: &[DiscoveredPool], ticks: &[u32]) { |
| 26 | + if pools.is_empty() && ticks.is_empty() { |
| 27 | + return; |
| 28 | + } |
| 29 | + if let Ok(mut cache) = self.pools.lock() { |
| 30 | + let entry = cache.entry(Self::pool_key(from, to)).or_default(); |
| 31 | + let mut seen: HashSet<String> = entry.pools.iter().map(|p| p.pool_id.clone()).collect(); |
| 32 | + for pool in pools { |
| 33 | + if seen.insert(pool.pool_id.clone()) { |
| 34 | + entry.pools.push(pool.clone()); |
| 35 | + } |
| 36 | + } |
| 37 | + for tick in ticks { |
| 38 | + if !entry.explored_ticks.contains(tick) { |
| 39 | + entry.explored_ticks.push(*tick); |
| 40 | + } |
| 41 | + } |
| 42 | + } |
16 | 43 | } |
17 | 44 |
|
18 | | - pub fn put(&self, from: &str, to: &str, pools: &[DiscoveredPool]) { |
19 | | - if pools.is_empty() { |
| 45 | + pub fn get_route(&self, from: &str, to: &str) -> Option<Vec<DiscoveredPool>> { |
| 46 | + let cache = self.routes.lock().ok()?; |
| 47 | + cache.get(&Self::route_key(from, to)).cloned() |
| 48 | + } |
| 49 | + |
| 50 | + pub fn put_route(&self, from: &str, to: &str, route: &[DiscoveredPool]) { |
| 51 | + if route.is_empty() { |
20 | 52 | return; |
21 | 53 | } |
22 | | - if let Ok(mut cache) = self.inner.lock() { |
23 | | - cache.insert(Self::key(from, to), pools.to_vec()); |
| 54 | + if let Ok(mut cache) = self.routes.lock() { |
| 55 | + cache.insert(Self::route_key(from, to), route.to_vec()); |
24 | 56 | } |
25 | 57 | } |
26 | 58 |
|
27 | | - fn key(from: &str, to: &str) -> (String, String) { |
| 59 | + fn pool_key(from: &str, to: &str) -> (String, String) { |
28 | 60 | let (a, b) = if from <= to { (from, to) } else { (to, from) }; |
29 | 61 | (a.to_string(), b.to_string()) |
30 | 62 | } |
| 63 | + |
| 64 | + fn route_key(from: &str, to: &str) -> (String, String) { |
| 65 | + (from.to_string(), to.to_string()) |
| 66 | + } |
31 | 67 | } |
32 | 68 |
|
33 | 69 | #[cfg(test)] |
34 | 70 | mod tests { |
35 | 71 | use super::*; |
36 | 72 |
|
| 73 | + fn pool(id: &str) -> DiscoveredPool { |
| 74 | + DiscoveredPool { |
| 75 | + pool_id: id.into(), |
| 76 | + pool_init_version: 1, |
| 77 | + coin_a: "0xa".into(), |
| 78 | + coin_b: "0xb".into(), |
| 79 | + } |
| 80 | + } |
| 81 | + |
37 | 82 | #[test] |
38 | | - fn test_key_is_direction_insensitive() { |
39 | | - assert_eq!(PoolCache::key("0xa", "0xb"), PoolCache::key("0xb", "0xa")); |
| 83 | + fn test_pool_key_is_direction_insensitive() { |
| 84 | + assert_eq!(PoolCache::pool_key("0xa", "0xb"), PoolCache::pool_key("0xb", "0xa")); |
| 85 | + } |
| 86 | + |
| 87 | + #[test] |
| 88 | + fn test_route_key_is_direction_sensitive() { |
| 89 | + assert_ne!(PoolCache::route_key("0xa", "0xb"), PoolCache::route_key("0xb", "0xa")); |
| 90 | + } |
| 91 | + |
| 92 | + #[test] |
| 93 | + fn test_pool_cache_merges_pools_and_ticks_across_passes() { |
| 94 | + let cache = PoolCache::default(); |
| 95 | + cache.put("0xa", "0xb", &[pool("0x1")], &[60, 200]); |
| 96 | + cache.put("0xa", "0xb", &[pool("0x2"), pool("0x1")], &[10, 2]); |
| 97 | + |
| 98 | + let (pools, ticks) = cache.get("0xa", "0xb").unwrap(); |
| 99 | + assert_eq!(pools.len(), 2); |
| 100 | + assert_eq!(pools[0].pool_id, "0x1"); |
| 101 | + assert_eq!(pools[1].pool_id, "0x2"); |
| 102 | + assert_eq!(ticks, vec![60, 200, 10, 2]); |
| 103 | + } |
| 104 | + |
| 105 | + #[test] |
| 106 | + fn test_pool_cache_tracks_explored_ticks_when_no_pools_found() { |
| 107 | + let cache = PoolCache::default(); |
| 108 | + cache.put("0xa", "0xb", &[], &[60, 200]); |
| 109 | + let (pools, ticks) = cache.get("0xa", "0xb").unwrap(); |
| 110 | + assert!(pools.is_empty()); |
| 111 | + assert_eq!(ticks, vec![60, 200]); |
| 112 | + } |
| 113 | + |
| 114 | + #[test] |
| 115 | + fn test_route_roundtrip() { |
| 116 | + let cache = PoolCache::default(); |
| 117 | + let route = vec![pool("0x1"), pool("0x2")]; |
| 118 | + cache.put_route("USDC", "WAL", &route); |
| 119 | + let fetched = cache.get_route("USDC", "WAL").expect("hit"); |
| 120 | + assert_eq!(fetched.len(), 2); |
| 121 | + assert!(cache.get_route("WAL", "USDC").is_none(), "direction-sensitive"); |
40 | 122 | } |
41 | 123 |
|
42 | 124 | #[test] |
43 | | - fn test_put_skips_empty_to_avoid_false_negative_cache() { |
| 125 | + fn test_put_route_skips_empty() { |
44 | 126 | let cache = PoolCache::default(); |
45 | | - cache.put("0xa", "0xb", &[]); |
46 | | - assert!(cache.get("0xa", "0xb").is_none()); |
| 127 | + cache.put_route("USDC", "WAL", &[]); |
| 128 | + assert!(cache.get_route("USDC", "WAL").is_none()); |
47 | 129 | } |
48 | 130 | } |
0 commit comments