this repo has no description

cached resolver

Orual cfedbbd0 91a84164

Changed files
+397 -64
crates
jacquard-identity
+85
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]] 207 218 name = "async-trait" 208 219 version = "0.1.89" 209 220 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 784 795 checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" 785 796 786 797 [[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]] 787 807 name = "console" 788 808 version = "0.15.11" 789 809 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 878 898 checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" 879 899 dependencies = [ 880 900 "cfg-if", 901 + ] 902 + 903 + [[package]] 904 + name = "crossbeam-channel" 905 + version = "0.5.15" 906 + source = "registry+https://github.com/rust-lang/crates.io-index" 907 + checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 908 + dependencies = [ 909 + "crossbeam-utils", 881 910 ] 882 911 883 912 [[package]] ··· 1299 1328 ] 1300 1329 1301 1330 [[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]] 1302 1352 name = "expect-json" 1303 1353 version = "1.5.0" 1304 1354 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2384 2434 "jacquard-common", 2385 2435 "jacquard-lexicon", 2386 2436 "miette", 2437 + "moka", 2387 2438 "n0-future", 2388 2439 "percent-encoding", 2389 2440 "reqwest", ··· 2928 2979 ] 2929 2980 2930 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 + ] 3001 + 3002 + [[package]] 2931 3003 name = "moxcms" 2932 3004 version = "0.7.7" 2933 3005 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3419 3491 "flate2", 3420 3492 "miniz_oxide 0.8.9", 3421 3493 ] 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" 3422 3500 3423 3501 [[package]] 3424 3502 name = "potential_utf" ··· 4694 4772 ] 4695 4773 4696 4774 [[package]] 4775 + name = "tagptr" 4776 + version = "0.2.0" 4777 + source = "registry+https://github.com/rust-lang/crates.io-index" 4778 + checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" 4779 + 4780 + [[package]] 4697 4781 name = "target-lexicon" 4698 4782 version = "0.12.16" 4699 4783 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 5327 5411 source = "registry+https://github.com/rust-lang/crates.io-index" 5328 5412 checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" 5329 5413 dependencies = [ 5414 + "getrandom 0.3.4", 5330 5415 "js-sys", 5331 5416 "wasm-bindgen", 5332 5417 ]
+1
Cargo.toml
··· 52 52 ipld-core = { version = "0.4.2", features = ["serde"] } 53 53 multihash = "0.19" 54 54 dashmap = "6.1" 55 + moka = "0.12" 55 56 56 57 # Proc macros 57 58 proc-macro2 = "1.0"
+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 20 20 21 [dependencies] 21 22 trait-variant.workspace = true ··· 36 37 urlencoding.workspace = true 37 38 tracing = { workspace = true, optional = true } 38 39 n0-future = { workspace = true, optional = true } 40 + moka = { workspace = true, features = ["future"], optional = true } 39 41 40 42 [target.'cfg(not(target_family = "wasm"))'.dependencies] 41 43 hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]}
+117 -51
crates/jacquard-identity/src/lexicon_resolver.rs
··· 6 6 7 7 use crate::resolver::{IdentityError, IdentityResolver}; 8 8 use jacquard_common::{ 9 - IntoStatic, smol_str, 9 + smol_str, 10 10 types::{cid::Cid, did::Did, string::Nsid}, 11 11 }; 12 12 use smol_str::SmolStr; ··· 223 223 for data in txt.txt_data().iter() { 224 224 let text = std::str::from_utf8(data).unwrap_or(""); 225 225 if let Some(did_str) = text.strip_prefix("did=") { 226 + use jacquard_common::IntoStatic; 227 + 226 228 return Did::new_owned(did_str) 227 229 .map(|d| d.into_static()) 228 230 .map_err(|_| LexiconResolutionError::invalid_did(authority, did_str)); ··· 240 242 &self, 241 243 nsid: &Nsid<'_>, 242 244 ) -> std::result::Result<Did<'static>, LexiconResolutionError> { 243 - self.resolve_lexicon_authority_dns(nsid).await 245 + // Try cache first 246 + #[cfg(feature = "cache")] 247 + if let Some(caches) = &self.caches { 248 + let authority = jacquard_common::smol_str::SmolStr::from(nsid.domain_authority()); 249 + if let Some(did) = caches.authority_to_did.get(&authority).await { 250 + return Ok(did); 251 + } 252 + } 253 + 254 + // Resolve via DNS 255 + let result = self.resolve_lexicon_authority_dns(nsid).await; 256 + 257 + // Cache on success, invalidate on error 258 + match &result { 259 + Ok(did) => 260 + { 261 + #[cfg(feature = "cache")] 262 + if let Some(caches) = &self.caches { 263 + let authority = 264 + jacquard_common::smol_str::SmolStr::from(nsid.domain_authority()); 265 + caches.authority_to_did.insert(authority, did.clone()).await; 266 + } 267 + } 268 + Err(_) => { 269 + #[cfg(feature = "cache")] 270 + self.invalidate_authority_chain(nsid.domain_authority()) 271 + .await; 272 + } 273 + } 274 + 275 + result 244 276 } 245 277 } 246 278 ··· 262 294 use jacquard_api::com_atproto::repo::get_record::GetRecord; 263 295 use jacquard_common::{IntoStatic, xrpc::XrpcExt}; 264 296 265 - // 1. Resolve authority DID via DNS 266 - let authority_did = self.resolve_lexicon_authority(nsid).await?; 297 + // Try cache first 298 + #[cfg(feature = "cache")] 299 + if let Some(caches) = &self.caches { 300 + let key = nsid.clone().into_static(); 301 + if let Some(schema) = caches.nsid_to_schema.get(&key).await { 302 + return Ok((*schema).clone()); 303 + } 304 + } 267 305 268 - #[cfg(feature = "tracing")] 269 - tracing::debug!( 270 - "resolved lexicon authority {} -> {}", 271 - nsid.domain_authority(), 272 - authority_did 273 - ); 306 + // Perform resolution 307 + let result = async { 308 + // 1. Resolve authority DID via DNS 309 + let authority_did = self.resolve_lexicon_authority(nsid).await?; 274 310 275 - // 2. Resolve DID document to get PDS endpoint 276 - let did_doc_resp = self.resolve_did_doc(&authority_did).await?; 277 - let did_doc = did_doc_resp.parse()?; 278 - let pds = did_doc 279 - .pds_endpoint() 280 - .ok_or_else(|| IdentityError::missing_pds_endpoint())?; 311 + #[cfg(feature = "tracing")] 312 + tracing::debug!( 313 + "resolved lexicon authority {} -> {}", 314 + nsid.domain_authority(), 315 + authority_did 316 + ); 281 317 282 - #[cfg(feature = "tracing")] 283 - tracing::debug!("fetching lexicon {} from PDS {}", nsid, pds); 318 + // 2. Resolve DID document to get PDS endpoint 319 + let did_doc_resp = self.resolve_did_doc(&authority_did).await?; 320 + let did_doc = did_doc_resp.parse()?; 321 + let pds = did_doc 322 + .pds_endpoint() 323 + .ok_or_else(|| IdentityError::missing_pds_endpoint())?; 284 324 285 - // 3. Fetch lexicon record via XRPC getRecord 286 - let collection = Nsid::new("com.atproto.lexicon.schema") 287 - .map_err(|_| LexiconResolutionError::invalid_collection())?; 325 + #[cfg(feature = "tracing")] 326 + tracing::debug!("fetching lexicon {} from PDS {}", nsid, pds); 288 327 289 - let request = GetRecord::new() 290 - .repo(authority_did.clone()) 291 - .collection(collection.into_static()) 292 - .rkey(nsid.clone()) 293 - .build(); 328 + // 3. Fetch lexicon record via XRPC getRecord 329 + let collection = Nsid::new("com.atproto.lexicon.schema") 330 + .map_err(|_| LexiconResolutionError::invalid_collection())?; 294 331 295 - let response = self 296 - .xrpc(pds) 297 - .send(&request) 298 - .await 299 - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 332 + let request = GetRecord::new() 333 + .repo(authority_did.clone()) 334 + .collection(collection.into_static()) 335 + .rkey(nsid.clone()) 336 + .build(); 300 337 301 - let output = response 302 - .into_output() 303 - .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 338 + let response = self 339 + .xrpc(pds) 340 + .send(&request) 341 + .await 342 + .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 304 343 305 - // 4. Parse lexicon document from value 306 - let json_str = serde_json::to_string(&output.value) 307 - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 344 + let output = response 345 + .into_output() 346 + .map_err(|e| LexiconResolutionError::fetch_failed(nsid.as_str(), e))?; 347 + 348 + // 4. Parse lexicon document from value 349 + let json_str = serde_json::to_string(&output.value) 350 + .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 351 + 352 + let doc: jacquard_lexicon::lexicon::LexiconDoc = serde_json::from_str(&json_str) 353 + .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 308 354 309 - let doc: jacquard_lexicon::lexicon::LexiconDoc = serde_json::from_str(&json_str) 310 - .map_err(|e| LexiconResolutionError::parse_failed(nsid.as_str(), e))?; 355 + #[cfg(feature = "tracing")] 356 + tracing::debug!("successfully parsed lexicon schema {}", nsid); 311 357 312 - #[cfg(feature = "tracing")] 313 - tracing::debug!("successfully parsed lexicon schema {}", nsid); 358 + let cid = output 359 + .cid 360 + .ok_or_else(|| LexiconResolutionError::missing_cid(nsid.as_str()))? 361 + .into_static(); 314 362 315 - let cid = output 316 - .cid 317 - .ok_or_else(|| LexiconResolutionError::missing_cid(nsid.as_str()))? 318 - .into_static(); 363 + Ok(ResolvedLexiconSchema { 364 + nsid: nsid.clone().into_static(), 365 + repo: authority_did.into_static(), 366 + cid, 367 + doc: doc.into_static(), 368 + }) 369 + } 370 + .await; 319 371 320 - Ok(ResolvedLexiconSchema { 321 - nsid: nsid.clone().into_static(), 322 - repo: authority_did.into_static(), 323 - cid, 324 - doc: doc.into_static(), 325 - }) 372 + // Handle result 373 + match result { 374 + Ok(schema) => { 375 + // Cache successful resolution 376 + #[cfg(feature = "cache")] 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; 382 + } 383 + Ok(schema) 384 + } 385 + Err(e) => { 386 + // Invalidate on error 387 + #[cfg(feature = "cache")] 388 + self.invalidate_lexicon_chain(nsid).await; 389 + Err(e) 390 + } 391 + } 326 392 } 327 393 }
+192 -13
crates/jacquard-identity/src/lib.rs
··· 96 96 std::sync::Arc, 97 97 }; 98 98 99 + #[cfg(feature = "cache")] 100 + use { 101 + crate::lexicon_resolver::ResolvedLexiconSchema, 102 + jacquard_common::{smol_str::SmolStr, types::string::Nsid}, 103 + moka::future::Cache, 104 + std::time::Duration, 105 + }; 106 + 107 + #[cfg(all( 108 + feature = "cache", 109 + not(all(feature = "dns", not(target_family = "wasm"))) 110 + ))] 111 + use std::sync::Arc; 112 + 113 + /// Cache layer for resolver operations 114 + #[cfg(feature = "cache")] 115 + #[derive(Clone)] 116 + struct ResolverCaches { 117 + handle_to_did: Cache<Handle<'static>, Did<'static>>, 118 + did_to_doc: Cache<Did<'static>, Arc<DidDocResponse>>, 119 + authority_to_did: Cache<SmolStr, Did<'static>>, 120 + nsid_to_schema: Cache<Nsid<'static>, Arc<ResolvedLexiconSchema<'static>>>, 121 + } 122 + 123 + #[cfg(feature = "cache")] 124 + impl ResolverCaches { 125 + fn new() -> Self { 126 + Self { 127 + handle_to_did: Cache::builder() 128 + .max_capacity(2000) 129 + .time_to_live(Duration::from_secs(24 * 3600)) 130 + .build(), 131 + did_to_doc: Cache::builder() 132 + .max_capacity(1000) 133 + .time_to_live(Duration::from_secs(72 * 3600)) 134 + .build(), 135 + authority_to_did: Cache::builder() 136 + .max_capacity(200) 137 + .time_to_live(Duration::from_secs(168 * 3600)) 138 + .build(), 139 + nsid_to_schema: Cache::builder() 140 + .max_capacity(500) 141 + .time_to_live(Duration::from_secs(168 * 3600)) 142 + .build(), 143 + } 144 + } 145 + } 146 + 99 147 /// Default resolver implementation with configurable fallback order. 100 148 #[derive(Clone)] 101 149 pub struct JacquardResolver { ··· 103 151 opts: ResolverOptions, 104 152 #[cfg(feature = "dns")] 105 153 dns: Option<Arc<TokioAsyncResolver>>, 154 + #[cfg(feature = "cache")] 155 + caches: Option<ResolverCaches>, 106 156 } 107 157 108 158 impl JacquardResolver { ··· 121 171 opts, 122 172 #[cfg(feature = "dns")] 123 173 dns: None, 174 + #[cfg(feature = "cache")] 175 + caches: None, 124 176 } 125 177 } 126 178 ··· 134 186 ResolverConfig::default(), 135 187 Default::default(), 136 188 ))), 189 + #[cfg(feature = "cache")] 190 + caches: None, 137 191 } 138 192 } 139 193 ··· 162 216 /// Enable/disable doc id validation 163 217 pub fn with_validate_doc_id(mut self, enable: bool) -> Self { 164 218 self.opts.validate_doc_id = enable; 219 + self 220 + } 221 + 222 + #[cfg(feature = "cache")] 223 + /// Enable caching with default configuration 224 + pub fn with_cache(mut self) -> Self { 225 + self.caches = Some(ResolverCaches::new()); 165 226 self 166 227 } 167 228 ··· 339 400 } 340 401 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(handle = %handle)))] 341 402 async fn resolve_handle(&self, handle: &Handle<'_>) -> resolver::Result<Did<'static>> { 403 + // Try cache first 404 + #[cfg(feature = "cache")] 405 + if let Some(caches) = &self.caches { 406 + let key = handle.clone().into_static(); 407 + if let Some(did) = caches.handle_to_did.get(&key).await { 408 + return Ok(did); 409 + } 410 + } 411 + 342 412 let host = handle.as_str(); 343 - for step in &self.opts.handle_order { 413 + let mut resolved_did: Option<Did<'static>> = None; 414 + 415 + 'outer: for step in &self.opts.handle_order { 344 416 match step { 345 417 HandleStep::DnsTxt => { 346 418 #[cfg(feature = "dns")] ··· 349 421 for txt in txts { 350 422 if let Some(did_str) = txt.strip_prefix("did=") { 351 423 if let Ok(did) = Did::new(did_str) { 352 - return Ok(did.into_static()); 424 + resolved_did = Some(did.into_static()); 425 + break 'outer; 353 426 } 354 427 } 355 428 } ··· 360 433 let url = Url::parse(&format!("https://{host}/.well-known/atproto-did"))?; 361 434 if let Ok(text) = self.get_text(url).await { 362 435 if let Ok(did) = Self::parse_atproto_did_body(&text) { 363 - return Ok(did); 436 + resolved_did = Some(did); 437 + break 'outer; 364 438 } 365 439 } 366 440 } 367 441 HandleStep::PdsResolveHandle => { 368 442 // Prefer PDS XRPC via stateless client 369 443 if let Ok(did) = self.resolve_handle_via_pds(handle).await { 370 - return Ok(did); 444 + resolved_did = Some(did); 445 + break 'outer; 371 446 } 372 447 // Public unauth fallback 373 448 if self.opts.public_fallback_for_handle { ··· 389 464 val.get("did").and_then(|v| v.as_str()) 390 465 { 391 466 if let Ok(did) = Did::new_owned(did_str) { 392 - return Ok(did.into_static()); 467 + resolved_did = Some(did.into_static()); 468 + break 'outer; 393 469 } 394 470 } 395 471 } ··· 413 489 if let Ok(val) = serde_json::from_slice::<serde_json::Value>(&buf) { 414 490 if let Some(did_str) = val.get("did").and_then(|v| v.as_str()) { 415 491 if let Ok(did) = Did::new_owned(did_str) { 416 - return Ok(did.into_static()); 492 + resolved_did = Some(did.into_static()); 493 + break 'outer; 417 494 } 418 495 } 419 496 } ··· 423 500 } 424 501 } 425 502 } 426 - Err(IdentityError::invalid_well_known()) 503 + 504 + // Handle result 505 + if let Some(did) = resolved_did { 506 + // Cache successful resolution 507 + #[cfg(feature = "cache")] 508 + if let Some(caches) = &self.caches { 509 + caches 510 + .handle_to_did 511 + .insert(handle.clone().into_static(), did.clone()) 512 + .await; 513 + } 514 + Ok(did) 515 + } else { 516 + // Invalidate on error 517 + #[cfg(feature = "cache")] 518 + self.invalidate_handle_chain(handle).await; 519 + Err(IdentityError::invalid_well_known()) 520 + } 427 521 } 428 522 429 523 #[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(self), fields(did = %did)))] 430 524 async fn resolve_did_doc(&self, did: &Did<'_>) -> resolver::Result<DidDocResponse> { 525 + // Try cache first 526 + #[cfg(feature = "cache")] 527 + if let Some(caches) = &self.caches { 528 + let key = did.clone().into_static(); 529 + if let Some(doc_resp) = caches.did_to_doc.get(&key).await { 530 + return Ok((*doc_resp).clone()); 531 + } 532 + } 533 + 431 534 let s = did.as_str(); 432 - for step in &self.opts.did_order { 535 + let mut resolved_doc: Option<DidDocResponse> = None; 536 + 537 + 'outer: for step in &self.opts.did_order { 433 538 match step { 434 539 DidStep::DidWebHttps if s.starts_with("did:web:") => { 435 540 let url = self.did_web_url(did)?; 436 541 if let Ok((buf, status)) = self.get_json_bytes(url).await { 437 - return Ok(DidDocResponse { 542 + resolved_doc = Some(DidDocResponse { 438 543 buffer: buf, 439 544 status, 440 545 requested: Some(did.clone().into_static()), 441 546 }); 547 + break 'outer; 442 548 } 443 549 } 444 550 DidStep::PlcHttp if s.starts_with("did:plc:") => { ··· 450 556 PlcSource::Slingshot { base } => base.join(did.as_str())?, 451 557 }; 452 558 if let Ok((buf, status)) = self.get_json_bytes(url).await { 453 - return Ok(DidDocResponse { 559 + resolved_doc = Some(DidDocResponse { 454 560 buffer: buf, 455 561 status, 456 562 requested: Some(did.clone().into_static()), 457 563 }); 564 + break 'outer; 458 565 } 459 566 } 460 567 DidStep::PdsResolveDid => { 461 568 // Try PDS XRPC for full DID doc 462 569 if let Ok(doc) = self.fetch_did_doc_via_pds_owned(did).await { 463 570 let buf = serde_json::to_vec(&doc).unwrap_or_default(); 464 - return Ok(DidDocResponse { 571 + resolved_doc = Some(DidDocResponse { 465 572 buffer: Bytes::from(buf), 466 573 status: StatusCode::OK, 467 574 requested: Some(did.clone().into_static()), 468 575 }); 576 + break 'outer; 469 577 } 470 578 // Fallback: if Slingshot configured, return mini-doc response (partial doc) 471 579 if let PlcSource::Slingshot { base } = &self.opts.plc_source { 472 580 let url = self.slingshot_mini_doc_url(base, did.as_str())?; 473 581 let (buf, status) = self.get_json_bytes(url).await?; 474 - return Ok(DidDocResponse { 582 + resolved_doc = Some(DidDocResponse { 475 583 buffer: buf, 476 584 status, 477 585 requested: Some(did.clone().into_static()), 478 586 }); 587 + break 'outer; 479 588 } 480 589 } 481 590 _ => {} 482 591 } 483 592 } 484 - Err(IdentityError::unsupported_did_method(s)) 593 + 594 + // Handle result 595 + if let Some(doc_resp) = resolved_doc { 596 + // Cache successful resolution 597 + #[cfg(feature = "cache")] 598 + if let Some(caches) = &self.caches { 599 + caches 600 + .did_to_doc 601 + .insert(did.clone().into_static(), Arc::new(doc_resp.clone())) 602 + .await; 603 + } 604 + Ok(doc_resp) 605 + } else { 606 + // Invalidate on error 607 + #[cfg(feature = "cache")] 608 + self.invalidate_did_chain(did).await; 609 + Err(IdentityError::unsupported_did_method(s)) 610 + } 485 611 } 486 612 } 487 613 ··· 587 713 urlencoding::Encoded::new(identifier) 588 714 ))); 589 715 Ok(url) 716 + } 717 + 718 + #[cfg(feature = "cache")] 719 + async fn invalidate_handle_chain(&self, handle: &Handle<'_>) { 720 + if let Some(caches) = &self.caches { 721 + let key = handle.clone().into_static(); 722 + caches.handle_to_did.invalidate(&key).await; 723 + } 724 + } 725 + 726 + #[cfg(feature = "cache")] 727 + async fn invalidate_did_chain(&self, did: &Did<'_>) { 728 + if let Some(caches) = &self.caches { 729 + let did_key = did.clone().into_static(); 730 + // Get doc before evicting to extract handles 731 + if let Some(doc_resp) = caches.did_to_doc.get(&did_key).await { 732 + let doc_resp_clone = (*doc_resp).clone(); 733 + if let Ok(doc) = doc_resp_clone.parse() { 734 + if let Some(aliases) = &doc.also_known_as { 735 + for alias in aliases { 736 + if let Some(handle_str) = alias.as_ref().strip_prefix("at://") { 737 + if let Ok(handle) = Handle::new(handle_str) { 738 + let handle_key = handle.into_static(); 739 + caches.handle_to_did.invalidate(&handle_key).await; 740 + } 741 + } 742 + } 743 + } 744 + } 745 + } 746 + caches.did_to_doc.invalidate(&did_key).await; 747 + } 748 + } 749 + 750 + #[cfg(feature = "cache")] 751 + async fn invalidate_authority_chain(&self, authority: &str) { 752 + if let Some(caches) = &self.caches { 753 + let authority = SmolStr::from(authority); 754 + caches.authority_to_did.invalidate(&authority).await; 755 + } 756 + } 757 + 758 + #[cfg(feature = "cache")] 759 + async fn invalidate_lexicon_chain(&self, nsid: &jacquard_common::types::string::Nsid<'_>) { 760 + if let Some(caches) = &self.caches { 761 + let nsid_key = nsid.clone().into_static(); 762 + if let Some(schema) = caches.nsid_to_schema.get(&nsid_key).await { 763 + let authority = SmolStr::from(nsid.domain_authority()); 764 + caches.authority_to_did.invalidate(&authority).await; 765 + self.invalidate_did_chain(&schema.repo).await; 766 + } 767 + caches.nsid_to_schema.invalidate(&nsid_key).await; 768 + } 590 769 } 591 770 592 771 /// Fetch a minimal DID document via Slingshot's mini-doc endpoint using a generic at-identifier