A human-friendly DSL for ATProto Lexicons
27
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add fallback for 3-segment authorities

+104 -11
+104 -11
mlf-cli/src/fetch.rs
··· 293 293 } 294 294 } 295 295 296 - // Retry failed wildcards with individual NSIDs 296 + // Retry failed wildcards. First try each pattern broadened by one 297 + // segment (handles 2-segment authorities like `rocksky.app` that 298 + // the 3-segment guess in `extract_namespace_pattern` misses). Only 299 + // fall through to individual NSID fetches when the broader pattern 300 + // also fails — those go through the same DNS path, so they're 301 + // unlikely to succeed if the pattern didn't, but we leave them in 302 + // place as a last resort. 297 303 if !wildcard_failures.is_empty() { 298 - println!("\n→ Retrying failed wildcard patterns with individual NSIDs..."); 304 + let mut still_failing: Vec<(String, Vec<String>)> = Vec::new(); 299 305 306 + println!("\n→ Retrying failed wildcard patterns with broader authority..."); 300 307 for (failed_pattern, nsids) in wildcard_failures { 301 - println!(" Retrying {} NSIDs from failed pattern: {}", nsids.len(), failed_pattern); 308 + match broaden_pattern(&failed_pattern) { 309 + Some(broader) if !fetched_nsids.contains(&broader) => { 310 + println!(" {} → {}", failed_pattern, broader); 311 + fetched_nsids.insert(broader.clone()); 312 + match fetch_lexicon_with_lock(&broader, project_root, lockfile).await { 313 + Ok(()) => continue, 314 + Err(e) => { 315 + eprintln!(" Warning: broader pattern {} also failed: {}", broader, e); 316 + } 317 + } 318 + } 319 + _ => {} 320 + } 321 + still_failing.push((failed_pattern, nsids)); 322 + } 302 323 303 - for nsid in nsids { 304 - if !fetched_nsids.contains(&nsid) { 305 - println!(" Fetching: {}", nsid); 306 - fetched_nsids.insert(nsid.clone()); 324 + if !still_failing.is_empty() { 325 + println!("\n→ Falling back to individual NSID fetches for patterns that couldn't be broadened..."); 326 + for (failed_pattern, nsids) in still_failing { 327 + println!(" Retrying {} NSIDs from failed pattern: {}", nsids.len(), failed_pattern); 307 328 308 - match fetch_lexicon_with_lock(&nsid, project_root, lockfile).await { 309 - Ok(()) => {} 310 - Err(e) => { 311 - eprintln!(" Warning: Failed to fetch {}: {}", nsid, e); 329 + for nsid in nsids { 330 + if !fetched_nsids.contains(&nsid) { 331 + println!(" Fetching: {}", nsid); 332 + fetched_nsids.insert(nsid.clone()); 333 + 334 + match fetch_lexicon_with_lock(&nsid, project_root, lockfile).await { 335 + Ok(()) => {} 336 + Err(e) => { 337 + eprintln!(" Warning: Failed to fetch {}: {}", nsid, e); 338 + } 312 339 } 313 340 } 314 341 } ··· 735 762 } 736 763 } 737 764 765 + /// Given a wildcard pattern, return a pattern one segment broader, or 766 + /// `None` if already at the 2-segment floor. 767 + /// 768 + /// ATProto authorities may have anywhere from 2 segments upward 769 + /// (`rocksky.app`, `bsky.app`, `repo.atproto.com`, ...). The extractor 770 + /// above guesses 3 segments for any NSID with ≥3 parts, which is right 771 + /// for most traffic (`app.bsky.*`, `com.atproto.*`) but wrong when the 772 + /// real authority has only 2 segments. Example: `app.rocksky.playlist.Foo` 773 + /// → guessed pattern `app.rocksky.playlist.*` → DNS at 774 + /// `_lexicon.playlist.rocksky.app` (NXDOMAIN); the correct fallback is 775 + /// `app.rocksky.*` → DNS at `_lexicon.rocksky.app`. 776 + fn broaden_pattern(pattern: &str) -> Option<String> { 777 + let prefix = pattern.strip_suffix(".*")?; 778 + let parts: Vec<&str> = prefix.split('.').collect(); 779 + if parts.len() <= 2 { 780 + return None; 781 + } 782 + let broader_prefix = parts[..parts.len() - 1].join("."); 783 + Some(format!("{}.*", broader_prefix)) 784 + } 785 + 786 + #[cfg(test)] 787 + mod tests { 788 + use super::*; 789 + 790 + #[test] 791 + fn extract_namespace_pattern_three_plus_segments() { 792 + assert_eq!( 793 + extract_namespace_pattern("app.bsky.actor.defs.profileViewBasic"), 794 + "app.bsky.actor.*" 795 + ); 796 + assert_eq!( 797 + extract_namespace_pattern("com.atproto.repo.strongRef"), 798 + "com.atproto.repo.*" 799 + ); 800 + } 801 + 802 + #[test] 803 + fn extract_namespace_pattern_two_segments() { 804 + assert_eq!(extract_namespace_pattern("place.stream"), "place.stream.*"); 805 + } 806 + 807 + #[test] 808 + fn broaden_pattern_drops_one_segment() { 809 + assert_eq!( 810 + broaden_pattern("app.rocksky.playlist.*").as_deref(), 811 + Some("app.rocksky.*") 812 + ); 813 + assert_eq!( 814 + broaden_pattern("com.atproto.repo.*").as_deref(), 815 + Some("com.atproto.*") 816 + ); 817 + } 818 + 819 + #[test] 820 + fn broaden_pattern_floors_at_two_segments() { 821 + assert_eq!(broaden_pattern("app.rocksky.*"), None); 822 + assert_eq!(broaden_pattern("place.stream.*"), None); 823 + } 824 + 825 + #[test] 826 + fn broaden_pattern_requires_wildcard_suffix() { 827 + assert_eq!(broaden_pattern("app.rocksky.playlist"), None); 828 + } 829 + } 830 +