A better Rust ATProto crate

swapped cache impl to mini-moka, sync on native, unsync in a mutex on wasm

Orual 024990b4 50dd180c

Changed files
+155 -118
crates
jacquard-identity
+20 -70
Cargo.lock
··· 204 204 ] 205 205 206 206 [[package]] 207 - name = "async-lock" 208 - version = "3.4.1" 209 - source = "registry+https://github.com/rust-lang/crates.io-index" 210 - checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 211 - dependencies = [ 212 - "event-listener", 213 - "event-listener-strategy", 214 - "pin-project-lite", 215 - ] 216 - 217 - [[package]] 218 207 name = "async-trait" 219 208 version = "0.1.89" 220 209 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 795 784 checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 796 785 797 786 [[package]] 798 - name = "concurrent-queue" 799 - version = "2.5.0" 800 - source = "registry+https://github.com/rust-lang/crates.io-index" 801 - checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" 802 - dependencies = [ 803 - "crossbeam-utils", 804 - ] 805 - 806 - [[package]] 807 787 name = "console" 808 788 version = "0.15.11" 809 789 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1328 1308 ] 1329 1309 1330 1310 [[package]] 1331 - name = "event-listener" 1332 - version = "5.4.1" 1333 - source = "registry+https://github.com/rust-lang/crates.io-index" 1334 - checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" 1335 - dependencies = [ 1336 - "concurrent-queue", 1337 - "parking", 1338 - "pin-project-lite", 1339 - ] 1340 - 1341 - [[package]] 1342 - name = "event-listener-strategy" 1343 - version = "0.5.4" 1344 - source = "registry+https://github.com/rust-lang/crates.io-index" 1345 - checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 1346 - dependencies = [ 1347 - "event-listener", 1348 - "pin-project-lite", 1349 - ] 1350 - 1351 - [[package]] 1352 1311 name = "expect-json" 1353 1312 version = "1.5.0" 1354 1313 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2434 2393 "jacquard-common", 2435 2394 "jacquard-lexicon", 2436 2395 "miette", 2437 - "moka", 2396 + "mini-moka", 2438 2397 "n0-future", 2439 2398 "percent-encoding", 2440 2399 "reqwest", ··· 2942 2901 ] 2943 2902 2944 2903 [[package]] 2904 + name = "mini-moka" 2905 + version = "0.11.0" 2906 + source = "git+https://github.com/moka-rs/mini-moka?rev=da864e849f5d034f32e02197fee9bb5d5af36d3d#da864e849f5d034f32e02197fee9bb5d5af36d3d" 2907 + dependencies = [ 2908 + "crossbeam-channel", 2909 + "crossbeam-utils", 2910 + "dashmap", 2911 + "smallvec", 2912 + "tagptr", 2913 + "triomphe", 2914 + ] 2915 + 2916 + [[package]] 2945 2917 name = "minimal-lexical" 2946 2918 version = "0.2.1" 2947 2919 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2976 2948 "libc", 2977 2949 "wasi", 2978 2950 "windows-sys 0.61.2", 2979 - ] 2980 - 2981 - [[package]] 2982 - name = "moka" 2983 - version = "0.12.11" 2984 - source = "registry+https://github.com/rust-lang/crates.io-index" 2985 - checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" 2986 - dependencies = [ 2987 - "async-lock", 2988 - "crossbeam-channel", 2989 - "crossbeam-epoch", 2990 - "crossbeam-utils", 2991 - "equivalent", 2992 - "event-listener", 2993 - "futures-util", 2994 - "parking_lot", 2995 - "portable-atomic", 2996 - "rustc_version", 2997 - "smallvec", 2998 - "tagptr", 2999 - "uuid", 3000 2951 ] 3001 2952 3002 2953 [[package]] ··· 3491 3442 "flate2", 3492 3443 "miniz_oxide 0.8.9", 3493 3444 ] 3494 - 3495 - [[package]] 3496 - name = "portable-atomic" 3497 - version = "1.11.1" 3498 - source = "registry+https://github.com/rust-lang/crates.io-index" 3499 - checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 3500 3445 3501 3446 [[package]] 3502 3447 name = "potential_utf" ··· 5239 5184 ] 5240 5185 5241 5186 [[package]] 5187 + name = "triomphe" 5188 + version = "0.1.15" 5189 + source = "registry+https://github.com/rust-lang/crates.io-index" 5190 + checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" 5191 + 5192 + [[package]] 5242 5193 name = "try-lock" 5243 5194 version = "0.2.5" 5244 5195 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5411 5362 source = "registry+https://github.com/rust-lang/crates.io-index" 5412 5363 checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 5413 5364 dependencies = [ 5414 - "getrandom 0.3.4", 5415 5365 "js-sys", 5416 5366 "wasm-bindgen", 5417 5367 ]
+1
Cargo.toml
··· 53 53 multihash = "0.19" 54 54 dashmap = "6.1" 55 55 moka = "0.12" 56 + mini-moka = "0.10" 56 57 57 58 # Proc macros 58 59 proc-macro2 = "1.0"
+2 -2
crates/jacquard-identity/Cargo.toml
··· 16 16 dns = ["dep:hickory-resolver"] 17 17 tracing = ["dep:tracing"] 18 18 streaming = ["jacquard-common/streaming", "dep:n0-future"] 19 - cache = ["dep:moka"] 19 + cache = ["dep:mini-moka"] 20 20 21 21 [dependencies] 22 22 trait-variant.workspace = true ··· 37 37 urlencoding.workspace = true 38 38 tracing = { workspace = true, optional = true } 39 39 n0-future = { workspace = true, optional = true } 40 + mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d", optional = true } 40 41 41 42 [target.'cfg(not(target_family = "wasm"))'.dependencies] 42 - moka = { workspace = true, features = ["future"], optional = true } 43 43 hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]} 44 44 tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } 45 45
+8 -7
crates/jacquard-identity/src/lexicon_resolver.rs
··· 246 246 #[cfg(feature = "cache")] 247 247 if let Some(caches) = &self.caches { 248 248 let authority = jacquard_common::smol_str::SmolStr::from(nsid.domain_authority()); 249 - if let Some(did) = caches.authority_to_did.get(&authority).await { 249 + if let Some(did) = crate::cache_impl::get(&caches.authority_to_did, &authority) { 250 250 return Ok(did); 251 251 } 252 252 } ··· 262 262 if let Some(caches) = &self.caches { 263 263 let authority = 264 264 jacquard_common::smol_str::SmolStr::from(nsid.domain_authority()); 265 - caches.authority_to_did.insert(authority, did.clone()).await; 265 + crate::cache_impl::insert(&caches.authority_to_did, authority, did.clone()); 266 266 } 267 267 } 268 268 Err(_) => { ··· 298 298 #[cfg(feature = "cache")] 299 299 if let Some(caches) = &self.caches { 300 300 let key = nsid.clone().into_static(); 301 - if let Some(schema) = caches.nsid_to_schema.get(&key).await { 301 + if let Some(schema) = crate::cache_impl::get(&caches.nsid_to_schema, &key) { 302 302 return Ok((*schema).clone()); 303 303 } 304 304 } ··· 375 375 // Cache successful resolution 376 376 #[cfg(feature = "cache")] 377 377 if let Some(caches) = &self.caches { 378 - caches 379 - .nsid_to_schema 380 - .insert(nsid.clone().into_static(), std::sync::Arc::new(schema.clone())) 381 - .await; 378 + crate::cache_impl::insert( 379 + &caches.nsid_to_schema, 380 + nsid.clone().into_static(), 381 + std::sync::Arc::new(schema.clone()), 382 + ); 382 383 } 383 384 Ok(schema) 384 385 }
+124 -39
crates/jacquard-identity/src/lib.rs
··· 100 100 use { 101 101 crate::lexicon_resolver::ResolvedLexiconSchema, 102 102 jacquard_common::{smol_str::SmolStr, types::string::Nsid}, 103 - moka::future::Cache, 104 103 std::time::Duration, 105 104 }; 106 105 ··· 110 109 ))] 111 110 use std::sync::Arc; 112 111 112 + // Platform-specific cache implementations 113 + #[cfg(all(feature = "cache", not(target_arch = "wasm32")))] 114 + mod cache_impl { 115 + /// Native: Use sync cache (thread-safe, no mutex needed) 116 + pub type Cache<K, V> = mini_moka::sync::Cache<K, V>; 117 + 118 + pub fn new_cache<K, V>(max_capacity: u64, ttl: std::time::Duration) -> Cache<K, V> 119 + where 120 + K: std::hash::Hash + Eq + Send + Sync + 'static, 121 + V: Clone + Send + Sync + 'static, 122 + { 123 + mini_moka::sync::Cache::builder() 124 + .max_capacity(max_capacity) 125 + .time_to_live(ttl) 126 + .build() 127 + } 128 + 129 + pub fn get<K, V>(cache: &Cache<K, V>, key: &K) -> Option<V> 130 + where 131 + K: std::hash::Hash + Eq + Send + Sync + 'static, 132 + V: Clone + Send + Sync + 'static, 133 + { 134 + cache.get(key) 135 + } 136 + 137 + pub fn insert<K, V>(cache: &Cache<K, V>, key: K, value: V) 138 + where 139 + K: std::hash::Hash + Eq + Send + Sync + 'static, 140 + V: Clone + Send + Sync + 'static, 141 + { 142 + cache.insert(key, value); 143 + } 144 + 145 + pub fn invalidate<K, V>(cache: &Cache<K, V>, key: &K) 146 + where 147 + K: std::hash::Hash + Eq + Send + Sync + 'static, 148 + V: Clone + Send + Sync + 'static, 149 + { 150 + cache.invalidate(key); 151 + } 152 + } 153 + 154 + #[cfg(all(feature = "cache", target_arch = "wasm32"))] 155 + mod cache_impl { 156 + use std::sync::{Arc, Mutex}; 157 + 158 + /// WASM: Use unsync cache in Arc<Mutex<_>> (no threads, but need interior mutability) 159 + pub type Cache<K, V> = Arc<Mutex<mini_moka::unsync::Cache<K, V>>>; 160 + 161 + pub fn new_cache<K, V>(max_capacity: u64, ttl: std::time::Duration) -> Cache<K, V> 162 + where 163 + K: std::hash::Hash + Eq + 'static, 164 + V: Clone + 'static, 165 + { 166 + Arc::new(Mutex::new( 167 + mini_moka::unsync::Cache::builder() 168 + .max_capacity(max_capacity) 169 + .time_to_live(ttl) 170 + .build(), 171 + )) 172 + } 173 + 174 + pub fn get<K, V>(cache: &Cache<K, V>, key: &K) -> Option<V> 175 + where 176 + K: std::hash::Hash + Eq + 'static, 177 + V: Clone + 'static, 178 + { 179 + cache.lock().unwrap().get(key).cloned() 180 + } 181 + 182 + pub fn insert<K, V>(cache: &Cache<K, V>, key: K, value: V) 183 + where 184 + K: std::hash::Hash + Eq + 'static, 185 + V: Clone + 'static, 186 + { 187 + cache.lock().unwrap().insert(key, value); 188 + } 189 + 190 + pub fn invalidate<K, V>(cache: &Cache<K, V>, key: &K) 191 + where 192 + K: std::hash::Hash + Eq + 'static, 193 + V: Clone + 'static, 194 + { 195 + cache.lock().unwrap().invalidate(key); 196 + } 197 + } 198 + 113 199 /// Configuration for resolver caching 114 200 #[cfg(feature = "cache")] 115 201 #[derive(Clone, Debug)] ··· 194 280 #[cfg(feature = "cache")] 195 281 #[derive(Clone)] 196 282 pub struct ResolverCaches { 197 - pub handle_to_did: Cache<Handle<'static>, Did<'static>>, 198 - pub did_to_doc: Cache<Did<'static>, Arc<DidDocResponse>>, 199 - pub authority_to_did: Cache<SmolStr, Did<'static>>, 200 - pub nsid_to_schema: Cache<Nsid<'static>, Arc<ResolvedLexiconSchema<'static>>>, 283 + pub handle_to_did: cache_impl::Cache<Handle<'static>, Did<'static>>, 284 + pub did_to_doc: cache_impl::Cache<Did<'static>, Arc<DidDocResponse>>, 285 + pub authority_to_did: cache_impl::Cache<SmolStr, Did<'static>>, 286 + pub nsid_to_schema: cache_impl::Cache<Nsid<'static>, Arc<ResolvedLexiconSchema<'static>>>, 201 287 } 202 288 203 289 #[cfg(feature = "cache")] 204 290 impl ResolverCaches { 205 291 pub fn new(config: &CacheConfig) -> Self { 206 292 Self { 207 - handle_to_did: Cache::builder() 208 - .max_capacity(config.handle_to_did_capacity) 209 - .time_to_live(config.handle_to_did_ttl) 210 - .build(), 211 - did_to_doc: Cache::builder() 212 - .max_capacity(config.did_to_doc_capacity) 213 - .time_to_live(config.did_to_doc_ttl) 214 - .build(), 215 - authority_to_did: Cache::builder() 216 - .max_capacity(config.authority_to_did_capacity) 217 - .time_to_live(config.authority_to_did_ttl) 218 - .build(), 219 - nsid_to_schema: Cache::builder() 220 - .max_capacity(config.nsid_to_schema_capacity) 221 - .time_to_live(config.nsid_to_schema_ttl) 222 - .build(), 293 + handle_to_did: cache_impl::new_cache( 294 + config.handle_to_did_capacity, 295 + config.handle_to_did_ttl, 296 + ), 297 + did_to_doc: cache_impl::new_cache(config.did_to_doc_capacity, config.did_to_doc_ttl), 298 + authority_to_did: cache_impl::new_cache( 299 + config.authority_to_did_capacity, 300 + config.authority_to_did_ttl, 301 + ), 302 + nsid_to_schema: cache_impl::new_cache( 303 + config.nsid_to_schema_capacity, 304 + config.nsid_to_schema_ttl, 305 + ), 223 306 } 224 307 } 225 308 } ··· 498 581 #[cfg(feature = "cache")] 499 582 if let Some(caches) = &self.caches { 500 583 let key = handle.clone().into_static(); 501 - if let Some(did) = caches.handle_to_did.get(&key).await { 584 + if let Some(did) = cache_impl::get(&caches.handle_to_did, &key) { 502 585 return Ok(did); 503 586 } 504 587 } ··· 600 683 // Cache successful resolution 601 684 #[cfg(feature = "cache")] 602 685 if let Some(caches) = &self.caches { 603 - caches 604 - .handle_to_did 605 - .insert(handle.clone().into_static(), did.clone()) 606 - .await; 686 + cache_impl::insert( 687 + &caches.handle_to_did, 688 + handle.clone().into_static(), 689 + did.clone(), 690 + ); 607 691 } 608 692 Ok(did) 609 693 } else { ··· 620 704 #[cfg(feature = "cache")] 621 705 if let Some(caches) = &self.caches { 622 706 let key = did.clone().into_static(); 623 - if let Some(doc_resp) = caches.did_to_doc.get(&key).await { 707 + if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &key) { 624 708 return Ok((*doc_resp).clone()); 625 709 } 626 710 } ··· 690 774 // Cache successful resolution 691 775 #[cfg(feature = "cache")] 692 776 if let Some(caches) = &self.caches { 693 - caches 694 - .did_to_doc 695 - .insert(did.clone().into_static(), Arc::new(doc_resp.clone())) 696 - .await; 777 + cache_impl::insert( 778 + &caches.did_to_doc, 779 + did.clone().into_static(), 780 + Arc::new(doc_resp.clone()), 781 + ); 697 782 } 698 783 Ok(doc_resp) 699 784 } else { ··· 813 898 async fn invalidate_handle_chain(&self, handle: &Handle<'_>) { 814 899 if let Some(caches) = &self.caches { 815 900 let key = handle.clone().into_static(); 816 - caches.handle_to_did.invalidate(&key).await; 901 + cache_impl::invalidate(&caches.handle_to_did, &key); 817 902 } 818 903 } 819 904 ··· 822 907 if let Some(caches) = &self.caches { 823 908 let did_key = did.clone().into_static(); 824 909 // Get doc before evicting to extract handles 825 - if let Some(doc_resp) = caches.did_to_doc.get(&did_key).await { 910 + if let Some(doc_resp) = cache_impl::get(&caches.did_to_doc, &did_key) { 826 911 let doc_resp_clone = (*doc_resp).clone(); 827 912 if let Ok(doc) = doc_resp_clone.parse() { 828 913 if let Some(aliases) = &doc.also_known_as { ··· 830 915 if let Some(handle_str) = alias.as_ref().strip_prefix("at://") { 831 916 if let Ok(handle) = Handle::new(handle_str) { 832 917 let handle_key = handle.into_static(); 833 - caches.handle_to_did.invalidate(&handle_key).await; 918 + cache_impl::invalidate(&caches.handle_to_did, &handle_key); 834 919 } 835 920 } 836 921 } 837 922 } 838 923 } 839 924 } 840 - caches.did_to_doc.invalidate(&did_key).await; 925 + cache_impl::invalidate(&caches.did_to_doc, &did_key); 841 926 } 842 927 } 843 928 ··· 845 930 async fn invalidate_authority_chain(&self, authority: &str) { 846 931 if let Some(caches) = &self.caches { 847 932 let authority = SmolStr::from(authority); 848 - caches.authority_to_did.invalidate(&authority).await; 933 + cache_impl::invalidate(&caches.authority_to_did, &authority); 849 934 } 850 935 } 851 936 ··· 853 938 async fn invalidate_lexicon_chain(&self, nsid: &jacquard_common::types::string::Nsid<'_>) { 854 939 if let Some(caches) = &self.caches { 855 940 let nsid_key = nsid.clone().into_static(); 856 - if let Some(schema) = caches.nsid_to_schema.get(&nsid_key).await { 941 + if let Some(schema) = cache_impl::get(&caches.nsid_to_schema, &nsid_key) { 857 942 let authority = SmolStr::from(nsid.domain_authority()); 858 - caches.authority_to_did.invalidate(&authority).await; 943 + cache_impl::invalidate(&caches.authority_to_did, &authority); 859 944 self.invalidate_did_chain(&schema.repo).await; 860 945 } 861 - caches.nsid_to_schema.invalidate(&nsid_key).await; 946 + cache_impl::invalidate(&caches.nsid_to_schema, &nsid_key); 862 947 } 863 948 } 864 949