This repository has no description
0

Configure Feed

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

bobbin-types: add global subject indicies

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
committer
dawn
date (Jul 30, 2026, 7:45 PM +0300) commit 2374400f parent 455141cd change-id xuoyvoxs
+95 -11
+90 -1
bobbin/crates/types/src/edges.rs
··· 160 160 let sort_micros = self.sort_micros_for(source); 161 161 let mut primary = self.primary_edges(source)?; 162 162 primary.iter_mut().for_each(|e| e.sort_micros = sort_micros); 163 - Ok(append_mirror_edges(primary, source)) 163 + let globals = global_edges(&primary); 164 + let mut out = append_mirror_edges(primary, source); 165 + out.extend(globals); 166 + Ok(out) 164 167 } 165 168 166 169 pub fn sort_micros_for(&self, source: &AtUri<DefaultStr>) -> u64 { ··· 282 285 source: source.clone(), 283 286 sort_micros: 0, 284 287 }] 288 + } 289 + 290 + const GLOBAL_KINDS: &[&str] = &[ 291 + "sh.tangled.feed.star", 292 + "sh.tangled.graph.follow", 293 + "sh.tangled.repo", 294 + ]; 295 + 296 + /// One `(kind, Global)` edge per allowlisted primary kind, sharing the primary's 297 + /// source + sort_micros. `seen` dedups if a record yields two primaries of a kind. 298 + fn global_edges(primary: &[Edge]) -> Vec<Edge> { 299 + let mut seen = std::collections::HashSet::new(); 300 + primary 301 + .iter() 302 + .filter(|e| GLOBAL_KINDS.contains(&e.kind.as_ref()) && seen.insert(e.kind.as_ref())) 303 + .map(|e| Edge { 304 + kind: e.kind.clone(), 305 + subject: SubjectRef::Global, 306 + source: e.source.clone(), 307 + sort_micros: e.sort_micros, 308 + }) 309 + .collect() 285 310 } 286 311 287 312 const MIRROR_KINDS: &[(&str, &str)] = &[ ··· 603 628 parsed.primary_edges(&at(source)).expect("extract") 604 629 } 605 630 631 + fn extract_full(collection: &'static str, source: &str, body: serde_json::Value) -> Vec<Edge> { 632 + let parsed = Record::from_json_value(&nsid(collection), body).expect("parse"); 633 + parsed.extract_edges(&at(source)).expect("extract") 634 + } 635 + 606 636 fn repo_body(source: Option<&str>) -> serde_json::Value { 607 637 let mut body = json!({ 608 638 "$type": "sh.tangled.repo", ··· 674 704 .all(|e| e.kind.as_ref() != REPO_SOURCE_EDGE_KIND), 675 705 "a clone url is not a repo we can count against", 676 706 ); 707 + } 708 + 709 + #[test] 710 + fn global_edge_emitted_for_allowlisted_kinds() { 711 + let cases = [ 712 + ( 713 + "sh.tangled.feed.star", 714 + "at://did:plc:olaren/sh.tangled.feed.star/abcabcabcabcz", 715 + json!({ 716 + "$type": "sh.tangled.feed.star", 717 + "createdAt": "2026-05-01T00:00:00Z", 718 + "subject": { "$type": "sh.tangled.feed.star#repo", "did": "did:plc:abalone" } 719 + }), 720 + ), 721 + ( 722 + "sh.tangled.graph.follow", 723 + "at://did:plc:olaren/sh.tangled.graph.follow/abcabcabcabcz", 724 + json!({ 725 + "$type": "sh.tangled.graph.follow", 726 + "createdAt": "2026-05-01T00:00:00Z", 727 + "subject": "did:plc:bailey" 728 + }), 729 + ), 730 + ( 731 + "sh.tangled.repo", 732 + "at://did:plc:olaren/sh.tangled.repo/abcabcabcabcz", 733 + json!({ 734 + "$type": "sh.tangled.repo", 735 + "createdAt": "2026-05-01T00:00:00Z", 736 + "knot": "knot.example.com", 737 + "name": "myrepo" 738 + }), 739 + ), 740 + ]; 741 + for (collection, source, body) in cases { 742 + let edges = extract_full(collection, source, body); 743 + let global: Vec<&Edge> = edges 744 + .iter() 745 + .filter(|e| e.subject == SubjectRef::Global) 746 + .collect(); 747 + assert_eq!(global.len(), 1, "{collection} should emit one global edge"); 748 + assert_eq!(global[0].kind, nsid(collection)); 749 + assert_eq!(global[0].source, at(source)); 750 + } 751 + } 752 + 753 + #[test] 754 + fn no_global_edge_for_non_allowlisted_kind() { 755 + let edges = extract_full( 756 + "sh.tangled.repo.issue", 757 + "at://did:plc:nel/sh.tangled.repo.issue/abcabcabcabcz", 758 + json!({ 759 + "$type": "sh.tangled.repo.issue", 760 + "repo": "did:plc:abalone", 761 + "title": "bug", 762 + "createdAt": "2026-05-01T00:00:00Z" 763 + }), 764 + ); 765 + assert!(edges.iter().all(|e| e.subject != SubjectRef::Global)); 677 766 } 678 767 679 768 #[test]
+4 -2
bobbin/crates/types/src/ids.rs
··· 8 8 pub enum SubjectRef { 9 9 Did(Did<DefaultStr>), 10 10 Uri(AtUri<DefaultStr>), 11 + Global, 11 12 } 12 13 13 14 impl SubjectRef { ··· 23 24 match self { 24 25 Self::Did(d) => d.as_ref(), 25 26 Self::Uri(u) => u.as_ref(), 27 + Self::Global => "*", 26 28 } 27 29 } 28 30 29 31 pub fn as_did(&self) -> Option<&Did<DefaultStr>> { 30 32 match self { 31 33 Self::Did(d) => Some(d), 32 - Self::Uri(_) => None, 34 + Self::Uri(_) | Self::Global => None, 33 35 } 34 36 } 35 37 36 38 pub fn as_uri(&self) -> Option<&AtUri<DefaultStr>> { 37 39 match self { 38 40 Self::Uri(u) => Some(u), 39 - Self::Did(_) => None, 41 + Self::Did(_) | Self::Global => None, 40 42 } 41 43 } 42 44 }
+1 -8
bobbin/crates/xrpc/src/enrich.rs
··· 133 133 } 134 134 } 135 135 if !per_ref.is_empty() { 136 - stats.insert(subject_string(reference), Value::Object(per_ref)); 136 + stats.insert(reference.as_str().to_owned(), Value::Object(per_ref)); 137 137 } 138 138 } 139 139 ··· 205 205 (SubjectShape::BareDidOrOneOfCollections(_), SubjectRef::Did(_)) => true, 206 206 (SubjectShape::AnyAtUri, SubjectRef::Uri(_)) => true, 207 207 _ => false, 208 - } 209 - } 210 - 211 - fn subject_string(reference: &SubjectRef) -> String { 212 - match reference { 213 - SubjectRef::Did(d) => d.as_ref().to_owned(), 214 - SubjectRef::Uri(u) => u.as_ref().to_owned(), 215 208 } 216 209 } 217 210