Monorepo for Tangled tangled.org
1

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
date (Jul 27, 2026, 8:17 PM +0900) commit 169c0319 parent 9aa625cd 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 { ··· 259 262 source: source.clone(), 260 263 sort_micros: 0, 261 264 }] 265 + } 266 + 267 + const GLOBAL_KINDS: &[&str] = &[ 268 + "sh.tangled.feed.star", 269 + "sh.tangled.graph.follow", 270 + "sh.tangled.repo", 271 + ]; 272 + 273 + /// One `(kind, Global)` edge per allowlisted primary kind, sharing the primary's 274 + /// source + sort_micros. `seen` dedups if a record yields two primaries of a kind. 275 + fn global_edges(primary: &[Edge]) -> Vec<Edge> { 276 + let mut seen = std::collections::HashSet::new(); 277 + primary 278 + .iter() 279 + .filter(|e| GLOBAL_KINDS.contains(&e.kind.as_ref()) && seen.insert(e.kind.as_ref())) 280 + .map(|e| Edge { 281 + kind: e.kind.clone(), 282 + subject: SubjectRef::Global, 283 + source: e.source.clone(), 284 + sort_micros: e.sort_micros, 285 + }) 286 + .collect() 262 287 } 263 288 264 289 const MIRROR_KINDS: &[(&str, &str)] = &[ ··· 578 603 fn extract(collection: &'static str, source: &str, body: serde_json::Value) -> Vec<Edge> { 579 604 let parsed = Record::from_json_value(&nsid(collection), body).expect("parse"); 580 605 parsed.primary_edges(&at(source)).expect("extract") 606 + } 607 + 608 + fn extract_full(collection: &'static str, source: &str, body: serde_json::Value) -> Vec<Edge> { 609 + let parsed = Record::from_json_value(&nsid(collection), body).expect("parse"); 610 + parsed.extract_edges(&at(source)).expect("extract") 611 + } 612 + 613 + #[test] 614 + fn global_edge_emitted_for_allowlisted_kinds() { 615 + let cases = [ 616 + ( 617 + "sh.tangled.feed.star", 618 + "at://did:plc:olaren/sh.tangled.feed.star/abcabcabcabcz", 619 + json!({ 620 + "$type": "sh.tangled.feed.star", 621 + "createdAt": "2026-05-01T00:00:00Z", 622 + "subject": { "$type": "sh.tangled.feed.star#repo", "did": "did:plc:abalone" } 623 + }), 624 + ), 625 + ( 626 + "sh.tangled.graph.follow", 627 + "at://did:plc:olaren/sh.tangled.graph.follow/abcabcabcabcz", 628 + json!({ 629 + "$type": "sh.tangled.graph.follow", 630 + "createdAt": "2026-05-01T00:00:00Z", 631 + "subject": "did:plc:bailey" 632 + }), 633 + ), 634 + ( 635 + "sh.tangled.repo", 636 + "at://did:plc:olaren/sh.tangled.repo/abcabcabcabcz", 637 + json!({ 638 + "$type": "sh.tangled.repo", 639 + "createdAt": "2026-05-01T00:00:00Z", 640 + "knot": "knot.example.com", 641 + "name": "myrepo" 642 + }), 643 + ), 644 + ]; 645 + for (collection, source, body) in cases { 646 + let edges = extract_full(collection, source, body); 647 + let global: Vec<&Edge> = edges 648 + .iter() 649 + .filter(|e| e.subject == SubjectRef::Global) 650 + .collect(); 651 + assert_eq!(global.len(), 1, "{collection} should emit one global edge"); 652 + assert_eq!(global[0].kind, nsid(collection)); 653 + assert_eq!(global[0].source, at(source)); 654 + } 655 + } 656 + 657 + #[test] 658 + fn no_global_edge_for_non_allowlisted_kind() { 659 + let edges = extract_full( 660 + "sh.tangled.repo.issue", 661 + "at://did:plc:nel/sh.tangled.repo.issue/abcabcabcabcz", 662 + json!({ 663 + "$type": "sh.tangled.repo.issue", 664 + "repo": "did:plc:abalone", 665 + "title": "bug", 666 + "createdAt": "2026-05-01T00:00:00Z" 667 + }), 668 + ); 669 + assert!(edges.iter().all(|e| e.subject != SubjectRef::Global)); 581 670 } 582 671 583 672 #[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