Monorepo for Tangled tangled.org
0

Configure Feed

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

Labels

None yet.

Participants 1
AT URI
at://did:plc:xasnlahkri4ewmbuzly2rlc5/sh.tangled.repo.pull/3mrmrvzsuqy22
+117
Diff #1
+1
Cargo.lock
··· 373 373 "bobbin-runtime", 374 374 "bobbin-types", 375 375 "either", 376 + "itertools 0.14.0", 376 377 "jacquard-common", 377 378 "lasso", 378 379 "scc",
+1
Cargo.toml
··· 57 57 tokio-tungstenite = { version = "0.29", features = ["rustls-tls-webpki-roots"] } 58 58 futures = "0.3" 59 59 either = "1" 60 + itertools = "0.14" 60 61 61 62 serde = { version = "1", features = ["derive"] } 62 63 serde_json = { version = "1", features = ["raw_value"] }
+1
bobbin/crates/edge-index/Cargo.toml
··· 9 9 bobbin-runtime = { workspace = true } 10 10 bobbin-types = { workspace = true } 11 11 either = { workspace = true } 12 + itertools = { workspace = true } 12 13 jacquard-common = { workspace = true } 13 14 lasso = { workspace = true } 14 15 scc = { workspace = true }
+114
bobbin/crates/edge-index/src/lib.rs
··· 28 28 use bobbin_types::edges::Edge; 29 29 use bobbin_types::ids::EdgeKey; 30 30 use either::Either; 31 + use itertools::Itertools; 31 32 use jacquard_common::DefaultStr; 32 33 use jacquard_common::types::string::AtUri; 33 34 use lasso::{Key, Spur, ThreadedRodeo}; ··· 733 734 }) 734 735 } 735 736 737 + /// Merge a page across many buckets in one global time order. 738 + pub fn list_multi( 739 + &self, 740 + keys: &[EdgeKey], 741 + cursor: PageCursor, 742 + limit: PageLimit, 743 + dir: SortDir, 744 + ) -> EdgePage { 745 + let n = limit.get() as usize + 1; 746 + let is_first = |x: &BucketKey, y: &BucketKey| match dir { 747 + SortDir::Asc => x <= y, 748 + SortDir::Desc => x >= y, 749 + }; 750 + let mut top: Vec<BucketKey> = Vec::with_capacity(n); 751 + for key in keys { 752 + let Some(id) = self.lookup_key(key) else { 753 + continue; 754 + }; 755 + let bucket: Vec<BucketKey> = self 756 + .forward 757 + .read_sync(&id, |_, sources| { 758 + sources.directed(cursor, dir).take(n).collect() 759 + }) 760 + .unwrap_or_default(); 761 + top = top.into_iter().merge_by(bucket, &is_first).take(n).collect(); 762 + } 763 + top.dedup_by_key(|k| k.source); 764 + 765 + let has_more = top.len() > limit.get() as usize; 766 + let page = &top[..top.len().min(limit.get() as usize)]; 767 + let items = page 768 + .iter() 769 + .filter_map(|&k| { 770 + let spur = k.source.to_spur()?; 771 + let stored = self.source_interner.try_resolve(&spur)?; 772 + let uri = AtUri::new_owned(self.decode_source(stored)?).ok()?; 773 + Some(EdgeItem { 774 + uri, 775 + sort_micros: k.micros.0, 776 + }) 777 + }) 778 + .collect(); 779 + let next = has_more 780 + .then(|| page.last().copied()) 781 + .flatten() 782 + .map(BucketKey::token); 783 + EdgePage { items, next } 784 + } 785 + 736 786 pub fn list_filtered<F>( 737 787 &self, 738 788 key: &EdgeKey, ··· 1063 1113 }); 1064 1114 } 1065 1115 1116 + // Build three subject buckets with interleaved micros so the global time 1117 + // order crosses buckets, and return the three keys. 1118 + fn fill_three_subjects(store: &EdgeStore) -> Vec<EdgeKey> { 1119 + let rows = [ 1120 + ("alpha", "r0", 10u64), 1121 + ("bravo", "r1", 50), 1122 + ("charlie", "r2", 20), 1123 + ("alpha", "r3", 40), 1124 + ("bravo", "r4", 30), 1125 + ("charlie", "r5", 60), 1126 + ]; 1127 + for (subj, rkey, micros) in rows { 1128 + let source = at(&format!("at://did:plc:x/sh.tangled.feed.star/{rkey}")); 1129 + store.add(star_edge_at(source, did(&format!("did:plc:{subj}")), micros)); 1130 + } 1131 + ["alpha", "bravo", "charlie"] 1132 + .iter() 1133 + .map(|s| { 1134 + EdgeKey::new(nsid("sh.tangled.feed.star"), did_subj(&format!("did:plc:{s}"))) 1135 + }) 1136 + .collect() 1137 + } 1138 + 1139 + #[test] 1140 + fn list_multi_merges_subjects_newest_first() { 1141 + let store = store(); 1142 + let keys = fill_three_subjects(&store); 1143 + let page = store.list_multi(&keys, PageCursor::Start, limit(10), SortDir::Desc); 1144 + let micros: Vec<u64> = page.items.iter().map(|it| it.sort_micros).collect(); 1145 + assert_eq!(micros, vec![60, 50, 40, 30, 20, 10]); 1146 + assert!(page.next.is_none()); 1147 + } 1148 + 1149 + #[test] 1150 + fn list_multi_paginates_disjoint_across_buckets() { 1151 + let store = store(); 1152 + let keys = fill_three_subjects(&store); 1153 + // limit=4 with 6 items across 3 buckets exercises the bounded top-N merge. 1154 + let p1 = store.list_multi(&keys, PageCursor::Start, limit(4), SortDir::Desc); 1155 + let m1: Vec<u64> = p1.items.iter().map(|it| it.sort_micros).collect(); 1156 + assert_eq!(m1, vec![60, 50, 40, 30]); 1157 + let tok = p1.next.expect("first page has more"); 1158 + let p2 = store.list_multi(&keys, PageCursor::After(tok), limit(4), SortDir::Desc); 1159 + let m2: Vec<u64> = p2.items.iter().map(|it| it.sort_micros).collect(); 1160 + assert_eq!(m2, vec![20, 10]); 1161 + assert!(p2.next.is_none()); 1162 + } 1163 + 1164 + #[test] 1165 + fn list_multi_dedups_source_under_two_keys() { 1166 + let store = store(); 1167 + // Same record (source) indexed under two subject keys, same createdAt. 1168 + let shared = at("at://did:plc:x/sh.tangled.feed.star/shared"); 1169 + store.add(star_edge_at(shared.clone(), did("did:plc:alpha"), 100)); 1170 + store.add(star_edge_at(shared, did("did:plc:bravo"), 100)); 1171 + let keys = vec![ 1172 + EdgeKey::new(nsid("sh.tangled.feed.star"), did_subj("did:plc:alpha")), 1173 + EdgeKey::new(nsid("sh.tangled.feed.star"), did_subj("did:plc:bravo")), 1174 + ]; 1175 + let page = store.list_multi(&keys, PageCursor::Start, limit(10), SortDir::Desc); 1176 + assert_eq!(page.items.len(), 1); 1177 + assert!(page.next.is_none()); 1178 + } 1179 + 1066 1180 #[test] 1067 1181 fn remove_from_large_bucket_keeps_pagination_exact() { 1068 1182 let store = store();

History

2 rounds 0 comments
Sign up or Login to add to the discussion
1 commit
Expand
bobbin-edge-index: list_multi
Checking mergeability…
Expand 0 comments
1 commit
Expand
bobbin-edge-index: list_multi
Expand 0 comments