Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

drop reverse many-to-many, don't expose for /links

https://tangled.org/microcosm.blue/microcosm-rs/pulls/6#comment-1444

many-to-many order is deterministic (could be reversed) but the order itself is arbitrary, so reversing it gives a different arbitrary order, but not a meaningfully different order.

the rocks impl also embeds assumptions about the paging rules into its join loop which might not hold

the regular /links endpoint won't get the reverse param because it's deprecated, but reversing backlinks is kept for the non-deprectated xrpc backlinks endpoint!

+18 -169
+2 -20
constellation/src/server/mod.rs
··· 239 239 /// Set the max number of links to return per page of results 240 240 #[serde(default = "get_default_cursor_limit")] 241 241 limit: u64, 242 - /// Allow returning links in reverse order (default: false) 243 - #[serde(default)] 244 - reverse: bool, 245 242 } 246 243 #[derive(Serialize)] 247 244 struct OtherSubjectCount { ··· 298 295 299 296 let path_to_other = format!(".{}", query.path_to_other); 300 297 301 - let order = if query.reverse { 302 - Order::OldestToNewest 303 - } else { 304 - Order::NewestToOldest 305 - }; 306 - 307 298 let paged = store 308 299 .get_many_to_many_counts( 309 300 &query.subject, 310 301 collection, 311 302 &path, 312 303 &path_to_other, 313 - order, 314 304 limit, 315 305 cursor_key, 316 306 &filter_dids, ··· 527 517 from_dids: Option<String>, // comma separated: gross 528 518 #[serde(default = "get_default_cursor_limit")] 529 519 limit: u64, 530 - /// Allow returning links in reverse order (default: false) 531 - #[serde(default)] 532 - reverse: bool, 533 520 } 534 521 #[derive(Template, Serialize)] 535 522 #[template(path = "links.html.j2")] ··· 578 565 } 579 566 } 580 567 581 - let order = if query.reverse { 582 - Order::OldestToNewest 583 - } else { 584 - Order::NewestToOldest 585 - }; 586 - 587 568 let paged = store 588 569 .get_links( 589 570 &query.target, 590 571 &query.collection, 591 572 &query.path, 592 - order, 573 + Order::NewestToOldest, 593 574 limit, 594 575 until, 595 576 &filter_dids, ··· 622 603 path: String, 623 604 cursor: Option<OpaqueApiCursor>, 624 605 limit: Option<u64>, 606 + // TODO: allow reverse (er, forward) order as well 625 607 } 626 608 #[derive(Template, Serialize)] 627 609 #[template(path = "dids.html.j2")]
+6 -17
constellation/src/storage/mem_store.rs
··· 1 1 use super::{ 2 - LinkReader, LinkStorage, Order, PagedAppendingCollection, PagedOrderedCollection, 3 - StorageStats, 2 + LinkReader, LinkStorage, Order, PagedAppendingCollection, PagedOrderedCollection, StorageStats, 4 3 }; 5 4 use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 6 5 use anyhow::Result; ··· 141 140 collection: &str, 142 141 path: &str, 143 142 path_to_other: &str, 144 - order: Order, 145 143 limit: u64, 146 144 after: Option<String>, 147 145 filter_dids: &HashSet<Did>, ··· 159 157 let filter_to_targets: HashSet<Target> = 160 158 HashSet::from_iter(filter_to_targets.iter().map(|s| Target::new(s))); 161 159 162 - // the last type field here acts as an index to allow keeping track of the order in which 163 - // we encountred single elements 164 - let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>, usize)> = HashMap::new(); 165 - for (idx, (did, rkey)) in linkers.iter().flatten().cloned().enumerate() { 160 + let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>)> = HashMap::new(); 161 + for (did, rkey) in linkers.iter().flatten().cloned() { 166 162 if !filter_dids.is_empty() && !filter_dids.contains(&did) { 167 163 continue; 168 164 } ··· 188 184 .take(1) 189 185 .next() 190 186 { 191 - let e = 192 - grouped_counts 193 - .entry(fwd_target.clone()) 194 - .or_insert((0, HashSet::new(), idx)); 187 + let e = grouped_counts.entry(fwd_target.clone()).or_default(); 195 188 e.0 += 1; 196 189 e.1.insert(did.clone()); 197 190 } 198 191 } 199 192 let mut items: Vec<(String, u64, u64)> = grouped_counts 200 193 .iter() 201 - .map(|(k, (n, u, _))| (k.0.clone(), *n, u.len() as u64)) 194 + .map(|(k, (n, u))| (k.0.clone(), *n, u.len() as u64)) 202 195 .collect(); 203 - // Sort based on order: OldestToNewest uses descending order, NewestToOldest uses ascending 204 - match order { 205 - Order::OldestToNewest => items.sort_by(|a, b| b.cmp(a)), 206 - Order::NewestToOldest => items.sort(), 207 - } 196 + items.sort(); 208 197 items = items 209 198 .into_iter() 210 199 .skip_while(|(t, _, _)| after.as_ref().map(|a| t <= a).unwrap_or(false))
-106
constellation/src/storage/mod.rs
··· 81 81 collection: &str, 82 82 path: &str, 83 83 path_to_other: &str, 84 - order: Order, 85 84 limit: u64, 86 85 after: Option<String>, 87 86 filter_dids: &HashSet<Did>, ··· 1508 1507 "a.b.c", 1509 1508 ".d.e", 1510 1509 ".f.g", 1511 - Order::NewestToOldest, 1512 1510 10, 1513 1511 None, 1514 1512 &HashSet::new(), ··· 1552 1550 "app.t.c", 1553 1551 ".abc.uri", 1554 1552 ".def.uri", 1555 - Order::NewestToOldest, 1556 1553 10, 1557 1554 None, 1558 1555 &HashSet::new(), ··· 1652 1649 "app.t.c", 1653 1650 ".abc.uri", 1654 1651 ".def.uri", 1655 - Order::NewestToOldest, 1656 1652 10, 1657 1653 None, 1658 1654 &HashSet::new(), ··· 1669 1665 "app.t.c", 1670 1666 ".abc.uri", 1671 1667 ".def.uri", 1672 - Order::NewestToOldest, 1673 1668 10, 1674 1669 None, 1675 1670 &HashSet::from_iter([Did("did:plc:fdsa".to_string())]), ··· 1686 1681 "app.t.c", 1687 1682 ".abc.uri", 1688 1683 ".def.uri", 1689 - Order::NewestToOldest, 1690 1684 10, 1691 1685 None, 1692 1686 &HashSet::new(), ··· 1697 1691 next: None, 1698 1692 } 1699 1693 ); 1700 - }); 1701 - 1702 - test_each_storage!(get_m2m_counts_reverse_order, |storage| { 1703 - // Create links from different DIDs to different targets 1704 - storage.push( 1705 - &ActionableEvent::CreateLinks { 1706 - record_id: RecordId { 1707 - did: "did:plc:user1".into(), 1708 - collection: "app.t.c".into(), 1709 - rkey: "post1".into(), 1710 - }, 1711 - links: vec![ 1712 - CollectedLink { 1713 - target: Link::Uri("a.com".into()), 1714 - path: ".abc.uri".into(), 1715 - }, 1716 - CollectedLink { 1717 - target: Link::Uri("b.com".into()), 1718 - path: ".def.uri".into(), 1719 - }, 1720 - ], 1721 - }, 1722 - 0, 1723 - )?; 1724 - storage.push( 1725 - &ActionableEvent::CreateLinks { 1726 - record_id: RecordId { 1727 - did: "did:plc:user2".into(), 1728 - collection: "app.t.c".into(), 1729 - rkey: "post1".into(), 1730 - }, 1731 - links: vec![ 1732 - CollectedLink { 1733 - target: Link::Uri("a.com".into()), 1734 - path: ".abc.uri".into(), 1735 - }, 1736 - CollectedLink { 1737 - target: Link::Uri("c.com".into()), 1738 - path: ".def.uri".into(), 1739 - }, 1740 - ], 1741 - }, 1742 - 1, 1743 - )?; 1744 - storage.push( 1745 - &ActionableEvent::CreateLinks { 1746 - record_id: RecordId { 1747 - did: "did:plc:user3".into(), 1748 - collection: "app.t.c".into(), 1749 - rkey: "post1".into(), 1750 - }, 1751 - links: vec![ 1752 - CollectedLink { 1753 - target: Link::Uri("a.com".into()), 1754 - path: ".abc.uri".into(), 1755 - }, 1756 - CollectedLink { 1757 - target: Link::Uri("d.com".into()), 1758 - path: ".def.uri".into(), 1759 - }, 1760 - ], 1761 - }, 1762 - 2, 1763 - )?; 1764 - 1765 - // Test NewestToOldest order (default order - by target ascending) 1766 - let counts = storage.get_many_to_many_counts( 1767 - "a.com", 1768 - "app.t.c", 1769 - ".abc.uri", 1770 - ".def.uri", 1771 - Order::NewestToOldest, 1772 - 10, 1773 - None, 1774 - &HashSet::new(), 1775 - &HashSet::new(), 1776 - )?; 1777 - assert_eq!(counts.items.len(), 3); 1778 - // Should be sorted by target in ascending order (alphabetical) 1779 - assert_eq!(counts.items[0].0, "b.com"); 1780 - assert_eq!(counts.items[1].0, "c.com"); 1781 - assert_eq!(counts.items[2].0, "d.com"); 1782 - 1783 - // Test OldestToNewest order (descending order - by target descending) 1784 - let counts = storage.get_many_to_many_counts( 1785 - "a.com", 1786 - "app.t.c", 1787 - ".abc.uri", 1788 - ".def.uri", 1789 - Order::OldestToNewest, 1790 - 10, 1791 - None, 1792 - &HashSet::new(), 1793 - &HashSet::new(), 1794 - )?; 1795 - assert_eq!(counts.items.len(), 3); 1796 - // Should be sorted by target in descending order (reverse alphabetical) 1797 - assert_eq!(counts.items[0].0, "d.com"); 1798 - assert_eq!(counts.items[1].0, "c.com"); 1799 - assert_eq!(counts.items[2].0, "b.com"); 1800 1694 }); 1801 1695 }
-8
constellation/src/storage/rocks_store.rs
··· 941 941 collection: &str, 942 942 path: &str, 943 943 path_to_other: &str, 944 - order: Order, 945 944 limit: u64, 946 945 after: Option<String>, 947 946 filter_dids: &HashSet<Did>, ··· 1072 1071 } 1073 1072 1074 1073 let mut items: Vec<(String, u64, u64)> = Vec::with_capacity(grouped_counts.len()); 1075 - 1076 1074 for (target_id, (n, dids)) in &grouped_counts { 1077 1075 let Some(target) = self 1078 1076 .target_id_table ··· 1082 1080 continue; 1083 1081 }; 1084 1082 items.push((target.0 .0, *n, dids.len() as u64)); 1085 - } 1086 - 1087 - // Sort based on order: OldestToNewest uses descending order, NewestToOldest uses ascending 1088 - match order { 1089 - Order::OldestToNewest => items.sort_by(|a, b| b.cmp(a)), // descending 1090 - Order::NewestToOldest => items.sort(), // ascending 1091 1083 } 1092 1084 1093 1085 let next = if grouped_counts.len() as u64 >= limit {
+4 -4
constellation/templates/base.html.j2
··· 40 40 padding: 0.5em 0.3em; 41 41 max-width: 100%; 42 42 } 43 - pre.code input { 44 - margin: 0; 45 - padding: 0; 46 - } 43 + pre.code input { 44 + margin: 0; 45 + padding: 0; 46 + } 47 47 .stat { 48 48 color: #f90; 49 49 font-size: 1.618rem;
-2
constellation/templates/get-many-to-many-counts.html.j2
··· 13 13 query.did, 14 14 query.other_subject, 15 15 query.limit, 16 - query.reverse, 17 16 ) %} 18 17 19 18 <h2> ··· 54 53 {% endfor %} 55 54 <input type="hidden" name="limit" value="{{ query.limit }}" /> 56 55 <input type="hidden" name="cursor" value={{ c|json|safe }} /> 57 - <input type="hidden" name="reverse" value="{{ query.reverse }}"> 58 56 <button type="submit">next page&hellip;</button> 59 57 </form> 60 58 {% else %}
+1 -3
constellation/templates/hello.html.j2
··· 70 70 <li><p><code>did</code>: optional, filter links to those from specific users. Include multiple times to filter by multiple users. Example: <code>did=did:plc:vc7f4oafdgxsihk4cry2xpze&did=did:plc:vc7f4oafdgxsihk4cry2xpze</code></p></li> 71 71 <li><p><code>otherSubject</code>: optional, filter secondary links to specific subjects. Include multiple times to filter by multiple users. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></p></li> 72 72 <li><p><code>limit</code>: optional. Default: <code>16</code>. Maximum: <code>100</code></p></li> 73 - <li><p><code>reverse</code>: optional, return links in reverse order. Default: <code>false</code></p></li> 74 73 </ul> 75 74 76 75 <p style="margin-bottom: 0"><strong>Try it:</strong></p> ··· 81 80 [""], 82 81 [""], 83 82 25, 84 - false, 85 83 ) %} 86 84 87 85 ··· 104 102 </ul> 105 103 106 104 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 107 - {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16, false) %} 105 + {% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %} 108 106 109 107 110 108 <h3 class="route"><code>GET /links/distinct-dids</code></h3>
+1 -2
constellation/templates/links.html.j2
··· 6 6 7 7 {% block content %} 8 8 9 - {% call try_it::links(query.target, query.collection, query.path, query.did, query.limit, query.reverse) %} 9 + {% call try_it::links(query.target, query.collection, query.path, query.did, query.limit) %} 10 10 11 11 <h2> 12 12 Links to <code>{{ query.target }}</code> ··· 37 37 <input type="hidden" name="collection" value="{{ query.collection }}" /> 38 38 <input type="hidden" name="path" value="{{ query.path }}" /> 39 39 <input type="hidden" name="cursor" value={{ c|json|safe }} /> 40 - <input type="hidden" name="reverse" value="{{ query.reverse }}"> 41 40 <button type="submit">next page&hellip;</button> 42 41 </form> 43 42 {% else %}
+4 -7
constellation/templates/try-it-macros.html.j2
··· 25 25 </script> 26 26 {% endmacro %} 27 27 28 - {% macro get_many_to_many_counts(subject, source, pathToOther, dids, otherSubjects, limit, reverse) %} 28 + {% macro get_many_to_many_counts(subject, source, pathToOther, dids, otherSubjects, limit) %} 29 29 <form method="get" action="/xrpc/blue.microcosm.links.getManyToManyCounts"> 30 30 <pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getManyToManyCounts 31 31 ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="at-uri, did, uri..." /> ··· 37 37 {%- for otherSubject in otherSubjects %}{% if !otherSubject.is_empty() %} 38 38 &otherSubject= <input type="text" name="did" value="{{ otherSubject }}" placeholder="at-uri, did, uri..." />{% endif %}{% endfor %} 39 39 <span id="m2m-did-placeholder"></span> <button id="m2m-add-did">+ did filter</button> 40 - &limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> 41 - &reverse= <input type="checkbox" name="reverse" value="true" checked="false"><button type="submit">get links</button></pre> 40 + &limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get links</button></pre> 42 41 </form> 43 42 <script> 44 43 const m2mAddDidButton = document.getElementById('m2m-add-did'); ··· 68 67 </script> 69 68 {% endmacro %} 70 69 71 - {% macro links(target, collection, path, dids, limit, reverse) %} 70 + {% macro links(target, collection, path, dids, limit) %} 72 71 <form method="get" action="/links"> 73 72 <pre class="code"><strong>GET</strong> /links 74 73 ?target= <input type="text" name="target" value="{{ target }}" placeholder="target" /> ··· 77 76 {%- for did in dids %}{% if !did.is_empty() %} 78 77 &did= <input type="text" name="did" value="{{ did }}" placeholder="did:plc:..." />{% endif %}{% endfor %} 79 78 <span id="did-placeholder"></span> <button id="add-did">+ did filter</button> 80 - &limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> 81 - &reverse= <input type="checkbox" name="reverse" value="true" checked="false"> 82 - <button type="submit">get links</button></pre> 79 + &limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get links</button></pre> 83 80 </form> 84 81 <script> 85 82 const addDidButton = document.getElementById('add-did');