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

Fix pagination logic for reverse-ordered link queries

The initial implementation of the reverse ordering feature had a flaw
in how it handled pagination. When `reverse=true` was set, the code
would reverse the entire collection before applying pagination
slicing, which broke the cursor logic. Cursor positions were
calculated relative to the original ordering but applied to a reversed
set of items, causing duplicate results or skipped items across page
boundaries.

The fix restructures pagination to account for reverse ordering:

- In reverse mode, pagination starts from the beginning (oldest items)
and moves forward, with begin/end indices calculated accordingly
- In normal mode, pagination continues working backward from the end
(newest items) as before
- The result slice is reversed __only after__ pagination boundaries
are established, ensuring cursor values remain consistent

Applied to both storage backends (mem_store and rocks_store) for
consistent behavior across implementations.

+45 -13
+26 -10
constellation/src/storage/mem_store.rs
··· 273 273 }); 274 274 }; 275 275 276 - let mut did_rkeys: Vec<_> = if !filter_dids.is_empty() { 276 + let did_rkeys: Vec<_> = if !filter_dids.is_empty() { 277 277 did_rkeys 278 278 .iter() 279 279 .filter(|m| { ··· 288 288 }; 289 289 290 290 let total = did_rkeys.len(); 291 - let end = until 292 - .map(|u| std::cmp::min(u as usize, total)) 293 - .unwrap_or(total); 294 - let begin = end.saturating_sub(limit as usize); 295 - let next = if begin == 0 { None } else { Some(begin as u64) }; 296 291 297 - let alive = did_rkeys.iter().flatten().count(); 298 - let gone = total - alive; 292 + let begin: usize; 293 + let end: usize; 294 + let next: Option<u64>; 299 295 300 296 if reverse { 301 - did_rkeys.reverse(); 297 + begin = until.map(|u| (u) as usize).unwrap_or(0); 298 + end = std::cmp::min(begin + limit as usize, total); 299 + 300 + next = if end < total { 301 + Some(end as u64 + 1) 302 + } else { 303 + None 304 + }; 305 + } else { 306 + end = until 307 + .map(|u| std::cmp::min(u as usize, total)) 308 + .unwrap_or(total); 309 + begin = end.saturating_sub(limit as usize); 310 + next = if begin == 0 { None } else { Some(begin as u64) }; 302 311 } 303 312 304 - let items: Vec<_> = did_rkeys[begin..end] 313 + let alive = did_rkeys.iter().flatten().count(); 314 + let gone = total - alive; 315 + 316 + let mut items: Vec<_> = did_rkeys[begin..end] 305 317 .iter() 306 318 .rev() 307 319 .flatten() ··· 312 324 collection: collection.to_string(), 313 325 }) 314 326 .collect(); 327 + 328 + if reverse { 329 + items.reverse(); 330 + } 315 331 316 332 Ok(PagedAppendingCollection { 317 333 version: (total as u64, gone as u64),
+19 -3
constellation/src/storage/rocks_store.rs
··· 1177 1177 1178 1178 let (alive, gone) = linkers.count(); 1179 1179 let total = alive + gone; 1180 - let end = until.map(|u| std::cmp::min(u, total)).unwrap_or(total) as usize; 1181 - let begin = end.saturating_sub(limit as usize); 1182 - let next = if begin == 0 { None } else { Some(begin as u64) }; 1180 + 1181 + let end: usize; 1182 + let begin: usize; 1183 + let next: Option<u64>; 1184 + 1185 + if reverse { 1186 + begin = until.map(|u| (u - 1) as usize).unwrap_or(0); 1187 + end = std::cmp::min(begin + limit as usize, total as usize); 1188 + 1189 + next = if end < total as usize { 1190 + Some(end as u64 + 1) 1191 + } else { 1192 + None 1193 + } 1194 + } else { 1195 + end = until.map(|u| std::cmp::min(u, total)).unwrap_or(total) as usize; 1196 + begin = end.saturating_sub(limit as usize); 1197 + next = if begin == 0 { None } else { Some(begin as u64) }; 1198 + } 1183 1199 1184 1200 let mut did_id_rkeys = linkers.0[begin..end].iter().rev().collect::<Vec<_>>(); 1185 1201