Signed-off-by: Seongmin Lee git@boltless.me
+95
-11
Diff
Round #1
+90
-1
bobbin/crates/types/src/edges.rs
+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 {
···
261
264
}]
262
265
}
263
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()
287
+
}
288
+
264
289
const MIRROR_KINDS: &[(&str, &str)] = &[
265
290
("sh.tangled.feed.comment", "sh.tangled.feed.comment.by"),
266
291
("sh.tangled.feed.star", "sh.tangled.feed.star.by"),
···
580
605
parsed.primary_edges(&at(source)).expect("extract")
581
606
}
582
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));
670
+
}
671
+
583
672
#[test]
584
673
fn star_repo_variant_keys_on_did() {
585
674
let edges = extract(
+4
-2
bobbin/crates/types/src/ids.rs
+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
+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
···
208
208
}
209
209
}
210
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
-
}
216
-
}
217
-
218
211
/// a value counts as a reference if it is a did string or an at-uri string
219
212
/// with collection and rkey, or a {uri, cid} strong ref
220
213
fn collect_ref(value: &Value, refs: &mut Vec<SubjectRef>, seen: &mut HashSet<SubjectRef>) {
History
2 rounds
0 comments
boltless.me
submitted
#1
1 commit
Expand
Collapse
bobbin-types: add global subject indicies
Signed-off-by: Seongmin Lee <git@boltless.me>
Expand 0 comments
boltless.me
submitted
#0
1 commit
Expand
Collapse
bobbin-types: add global subject indicies
Signed-off-by: Seongmin Lee <git@boltless.me>