Something went wrong. Try again.
atproto Thingiverse but good
Something went wrong. Try again.
Rust
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433//! SQLite read queries and generated-view builders for the read endpoints.//!//! Records come from the `record_json` column (written by the projection); the//! required `record` field on thing/model/part views is built from it. Author//! handles come from `identities` (refreshed by identity events) and profile//! display fields from `profiles`. Authored image/blob fields are deserialized//! through generated lexicon types and resolved to Bluesky CDN URLs in appview//! response fields; no fake CDN/media data is synthesized.//!//! Cursor contract: clients treat `cursor` as opaque.//! - Chronological feeds (`getAuthorThings`, `getFeed?algorithm=recent`) keyset//! on the record's TID `rkey` (descending); the returned cursor is the last//! item's `rkey`, and the next page uses `rkey < cursor` (a NULL cursor — no//! cursor — returns the first page).//! - `getFeed?algorithm=hot` uses an opaque versioned keyset cursor over a hot//! ranking snapshot. The cursor pins the ranking timestamp and last returned//! `(score, rkey, uri)` boundary so later pages use the same total ordering.//! - `searchThings` uses a stringified offset cursor for BM25 results; a//! non-numeric cursor is rejected with 400.
use std::cmp::Ordering;
use jacquard::identity::PublicResolver;use jacquard::identity::resolver::IdentityResolver;use jacquard::oauth::client::OAuthSession;use jacquard_common::deps::smol_str::SmolStr;use jacquard_common::types::string::{AtUri, Datetime, Did, Handle, UriValue};use jacquard_common::types::{blob::BlobRef, value::Data};use polymodel_api::space_polymodel::actor::ProfileListView;use polymodel_api::space_polymodel::library::{ Actor, ActorViewerState, FeedItem, FeedView, File, Image, ImageView, ModelView, PartView, ThingView, ThingViewBasic, ViewerState, model::Model, part::Part, thing::Thing,};use sqlx::SqlitePool;
use super::error::{AppError, AppResult, db, internal, invalid_request, not_found};use super::state::AppState;use crate::oauth::SqliteAuthStore;
const HOT_POSITIVE_BATCH: i64 = 256;
// ---------------------------------------------------------------------------// value-construction helpers// ---------------------------------------------------------------------------
fn s(v: String) -> SmolStr { SmolStr::from(v)}
fn at_uri(uri: &str) -> AppResult<AtUri> { AtUri::new_owned(uri).map_err(|e| invalid_request(format!("invalid at-uri {uri:?}: {e}")))}
fn did_of(did: &str) -> AppResult<Did> { Did::new_owned(did).map_err(|e| internal(format!("invalid did {did:?}: {e}")))}fn handle_of(handle: String) -> AppResult<Handle> { Handle::new_owned(handle).map_err(|e| internal(format!("invalid handle: {e}")))}fn datetime_nanos(nanos: i64) -> Datetime { let dt = chrono::DateTime::<chrono::Utc>::from_timestamp_nanos(nanos); Datetime::new(dt.fixed_offset())}
/// Build the required `record: Data` field from a stored `record_json` blob.fn record_data(record_json: Option<&str>) -> AppResult<Data> { let raw = record_json.ok_or_else(|| internal("content row missing record_json"))?; serde_json::from_str::<Data>(raw).map_err(|e| internal(format!("record_json parse: {e}")))}
fn file_data(file_json: Option<&str>) -> AppResult<File> { serde_json::from_str(file_json.ok_or_else(|| internal("part row missing file_json"))?) .map_err(|e| internal(format!("file_json parse: {e}")))}fn smol_vec(json: Option<&str>) -> Option<Vec<SmolStr>> { let arr: Vec<String> = serde_json::from_str(json?).ok()?; let v: Vec<SmolStr> = arr.into_iter().map(s).collect(); if v.is_empty() { None } else { Some(v) }}
fn uri_vec(json: Option<&str>) -> Option<Vec<UriValue>> { let arr: Vec<String> = serde_json::from_str(json?).ok()?; arr.into_iter() .map(|u| UriValue::new_owned(u).ok()) .collect::<Option<Vec<_>>>() .filter(|v| !v.is_empty())}fn blob_cdn_url(repo_did: &str, blob_ref: &BlobRef) -> AppResult<UriValue> { let cid = blob_ref.blob().r#ref.as_str(); UriValue::new_owned(format!( "https://cdn.bsky.app/img/feed_thumbnail/plain/{repo_did}/{cid}@jpeg" )) .map_err(|e| internal(format!("invalid generated image CDN URL: {e}")))}
fn avatar_cdn_url(repo_did: &str, blob_ref: &BlobRef) -> AppResult<UriValue> { let cid = blob_ref.blob().r#ref.as_str(); UriValue::new_owned(format!( "https://cdn.bsky.app/img/avatar/plain/{repo_did}/{cid}@jpeg" )) .map_err(|e| internal(format!("invalid generated avatar CDN URL: {e}")))}
fn image_view(repo_did: &str, image: Image) -> AppResult<ImageView> { let url = blob_cdn_url(repo_did, &image.image)?; Ok(ImageView { alt: Some(image.alt), aspect_ratio: Some(image.aspect_ratio), fullsize: url.clone(), thumb: url, extra_data: None, })}
fn image_views_from_images( repo_did: &str, images: Option<Vec<Image>>,) -> AppResult<Option<Vec<ImageView>>> { images .unwrap_or_default() .into_iter() .map(|image| image_view(repo_did, image)) .collect::<AppResult<Vec<_>>>() .map(|views| if views.is_empty() { None } else { Some(views) })}
fn thing_record(record_json: Option<&str>) -> AppResult<Thing> { serde_json::from_str(record_json.ok_or_else(|| internal("thing row missing record_json"))?) .map_err(|e| internal(format!("thing record_json typed parse: {e}")))}
fn model_record(record_json: Option<&str>) -> AppResult<Model> { serde_json::from_str(record_json.ok_or_else(|| internal("model row missing record_json"))?) .map_err(|e| internal(format!("model record_json typed parse: {e}")))}
fn part_record(record_json: Option<&str>) -> AppResult<Part> { serde_json::from_str(record_json.ok_or_else(|| internal("part row missing record_json"))?) .map_err(|e| internal(format!("part record_json typed parse: {e}")))}
fn avatar_url(repo_did: &str, avatar_json: Option<&str>) -> AppResult<Option<UriValue>> { let Some(raw) = avatar_json else { return Ok(None); }; let blob_ref: BlobRef = serde_json::from_str(raw).map_err(|e| internal(format!("avatar_json parse: {e}")))?; Ok(Some(avatar_cdn_url(repo_did, &blob_ref)?))}// ---------------------------------------------------------------------------// rows// ---------------------------------------------------------------------------
#[allow(dead_code)] // query-result row; mirrors projected columns#[derive(sqlx::FromRow)]struct ThingRow { did: String, rkey: String, uri: String, cid: String, name: String, summary: Option<String>, license: String, tags_json: Option<String>, cover_json: Option<String>, derived_from_uri: Option<String>, created_at: i64, indexed_at: i64, record_json: Option<String>, like_count: i64, save_count: i64, model_count: i64, part_count: i64,}#[allow(dead_code)] // query-result row; mirrors projected columns#[derive(sqlx::FromRow)]struct ModelRow { did: String, rkey: String, uri: String, cid: String, name: String, summary: Option<String>, created_at: i64, indexed_at: i64, record_json: Option<String>, part_count: i64,}
#[allow(dead_code)] // query-result row; mirrors projected columns#[derive(sqlx::FromRow)]struct PartRow { did: String, rkey: String, uri: String, cid: String, name: String, format: Option<String>, file_json: String, created_at: i64, indexed_at: i64, record_json: Option<String>,}
#[allow(dead_code)] // query-result row; mirrors projected columns#[derive(sqlx::FromRow)]struct ProfileRow { did: String, display_name: Option<String>, description: Option<String>, avatar_json: Option<String>, default_license: Option<String>, pronouns: Option<String>, printers_json: Option<String>, links_json: Option<String>, record_json: String, indexed_at: i64,}
// ---------------------------------------------------------------------------// author + viewer// ---------------------------------------------------------------------------
async fn actor_for_did(state: &AppState, did: &str) -> AppResult<Actor> { #[derive(sqlx::FromRow)] struct AuthorRow { handle: Option<String>, display_name: Option<String>, avatar_json: Option<String>, } let row = db(sqlx::query_as!( AuthorRow, r#"SELECT (SELECT handle FROM identities WHERE did = ?) AS "handle?", (SELECT display_name FROM profiles WHERE did = ?) AS "display_name?", (SELECT avatar_json FROM profiles WHERE did = ?) AS "avatar_json?""#, did, did, did, ) .fetch_optional(&state.pool) .await)?; let (handle, display_name, avatar_json) = match row { Some(r) => (r.handle, r.display_name, r.avatar_json), None => (None, None, None), }; let did_typed = did_of(did)?; // Read-your-own-writes / not-yet-indexed authors: fall back to resolving the // handle (local cache, then DID document) instead of 500-ing the read. let handle = match handle { Some(h) => h, None => resolve_handle(state, &did_typed).await?, }; let stats = actor_follow_stats(&state.pool, did, None).await?; Ok(Actor { did: did_typed, handle: handle_of(handle)?, display_name: display_name.map(s), avatar: avatar_url(did, avatar_json.as_deref())?, description: None, follower_count: Some(stats.follower_count), following_count: Some(stats.following_count), pronouns: None, thing_count: None, viewer: None, extra_data: None, })}
struct ActorFollowStats { follower_count: i64, following_count: i64, viewer: ActorViewerState,}
async fn actor_follow_stats( pool: &SqlitePool, actor_did: &str, viewer_did: Option<Did<&str>>,) -> AppResult<ActorFollowStats> { #[derive(sqlx::FromRow)] struct CountRow { count: i64, } let follower_count = db(sqlx::query_as!( CountRow, r#"SELECT COUNT(*) AS "count!: i64" FROM follows WHERE followed_did = ?"#, actor_did, ) .fetch_one(pool) .await)? .count; let following_count = db(sqlx::query_as!( CountRow, r#"SELECT COUNT(*) AS "count!: i64" FROM follows WHERE follower_did = ?"#, actor_did, ) .fetch_one(pool) .await)? .count;
let Some(viewer_did) = viewer_did else { return Ok(ActorFollowStats { follower_count, following_count, viewer: ActorViewerState::default(), }); }; let viewer_did = viewer_did.as_str(); #[derive(sqlx::FromRow)] struct RkeyRow { rkey: String, } let following = db(sqlx::query_as!( RkeyRow, "SELECT rkey FROM follows WHERE follower_did = ? AND followed_did = ?", viewer_did, actor_did, ) .fetch_optional(pool) .await)?; let followed_by = db(sqlx::query_as!( RkeyRow, "SELECT rkey FROM follows WHERE follower_did = ? AND followed_did = ?", actor_did, viewer_did, ) .fetch_optional(pool) .await)?; let following = following .map(|row| { at_uri(&format!( "at://{viewer_did}/space.polymodel.graph.follow/{}", row.rkey )) }) .transpose()?; let followed_by = followed_by .map(|row| { at_uri(&format!( "at://{actor_did}/space.polymodel.graph.follow/{}", row.rkey )) }) .transpose()?;
Ok(ActorFollowStats { follower_count, following_count, viewer: ActorViewerState { following, followed_by, extra_data: None, ..ActorViewerState::default() }, })}
async fn viewer_state( pool: &SqlitePool, viewer_did: Option<Did<&str>>, subject_uri: &str,) -> AppResult<ViewerState> { let Some(did) = viewer_did else { return Ok(ViewerState::default()); }; let did = did.as_str(); #[derive(sqlx::FromRow)] struct IdRow { rkey: String, } let like = db(sqlx::query_as!( IdRow, "SELECT rkey FROM likes WHERE did = ? AND subject_uri = ?", did, subject_uri ) .fetch_optional(pool) .await)?; let save = db(sqlx::query_as!( IdRow, "SELECT rkey FROM saves WHERE did = ? AND subject_uri = ?", did, subject_uri ) .fetch_optional(pool) .await)?; if like.is_none() && save.is_none() { return Ok(ViewerState::default()); } let like = like .map(|r| at_uri(&format!("at://{did}/space.polymodel.graph.like/{}", r.rkey))) .transpose()?; let save = save .map(|r| at_uri(&format!("at://{did}/space.polymodel.graph.save/{}", r.rkey))) .transpose()?; Ok(ViewerState { like, save, extra_data: None, })}
pub(super) async fn viewer_did( session: Option<&OAuthSession<PublicResolver, SqliteAuthStore>>,) -> Option<Did> { match session { Some(s) => Some(s.session_info().await.0), None => None, }}
// ---------------------------------------------------------------------------// view builders// ---------------------------------------------------------------------------
async fn thing_view( state: &AppState, row: ThingRow, viewer_did: Option<Did<&str>>,) -> AppResult<ThingView> { let author = actor_for_did(state, &row.did).await?; let viewer = viewer_state(&state.pool, viewer_did, &row.uri).await?; let record = thing_record(row.record_json.as_deref())?; Ok(ThingView { uri: at_uri(&row.uri)?, cid: s(row.cid), author, record: record_data(row.record_json.as_deref())?, indexed_at: datetime_nanos(row.indexed_at), name: s(row.name), summary: row.summary.map(s), license: s(row.license), tags: smol_vec(row.tags_json.as_deref()), like_count: row.like_count, save_count: row.save_count, model_count: row.model_count, part_count: row.part_count, viewer, cover: image_views_from_images(&row.did, record.cover)?, previews: image_views_from_images(&row.did, record.previews)?, derived_from: None, intended_to_work_with: None, extra_data: None, })}
async fn thing_basic_view( state: &AppState, row: ThingRow, viewer_did: Option<Did<&str>>,) -> AppResult<ThingViewBasic> { let record = thing_record(row.record_json.as_deref())?; Ok(ThingViewBasic { uri: at_uri(&row.uri)?, cid: s(row.cid), name: s(row.name), author: actor_for_did(state, &row.did).await?, indexed_at: datetime_nanos(row.indexed_at), like_count: row.like_count, save_count: row.save_count, model_count: row.model_count, part_count: row.part_count, cover: image_views_from_images(&row.did, record.cover)?, previews: image_views_from_images(&row.did, record.previews)?, tags: smol_vec(row.tags_json.as_deref()), viewer: viewer_state(&state.pool, viewer_did, &row.uri).await?, extra_data: None, })}
async fn model_view( state: &AppState, row: ModelRow, viewer_did: Option<Did<&str>>,) -> AppResult<ModelView> { let author = actor_for_did(state, &row.did).await?; let viewer = viewer_state(&state.pool, viewer_did, &row.uri).await?; let record = model_record(row.record_json.as_deref())?; Ok(ModelView { uri: at_uri(&row.uri)?, cid: s(row.cid), author, record: record_data(row.record_json.as_deref())?, indexed_at: datetime_nanos(row.indexed_at), name: s(row.name), summary: row.summary.map(s), part_count: row.part_count, viewer, cover: image_views_from_images(&row.did, record.cover)?, previews: image_views_from_images(&row.did, record.previews)?, extra_data: None, })}
async fn part_view( state: &AppState, row: PartRow, viewer_did: Option<Did<&str>>,) -> AppResult<PartView> { let author = actor_for_did(state, &row.did).await?; let viewer = viewer_state(&state.pool, viewer_did, &row.uri).await?; let record = part_record(row.record_json.as_deref())?; Ok(PartView { uri: at_uri(&row.uri)?, cid: s(row.cid), author, record: record_data(row.record_json.as_deref())?, indexed_at: datetime_nanos(row.indexed_at), name: s(row.name), format: row.format.map(s), file: file_data(Some(row.file_json.as_str()))?, previews: image_views_from_images(&row.did, record.previews)?, viewer, extra_data: None, })}
// ---------------------------------------------------------------------------// getThing / getModel lookups// ---------------------------------------------------------------------------
/// Fetch a thing row by URI (with stats + model count).async fn fetch_thing_by_uri(state: &AppState, uri: &str) -> AppResult<Option<ThingRow>> { db(sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE t.uri = ?"#, uri, ) .fetch_optional(&state.pool) .await)}
pub(crate) async fn get_thing( state: &AppState, uri: &str, viewer_did: Option<Did<&str>>,) -> AppResult<(ThingView, Vec<ModelView>)> { let row = fetch_thing_by_uri(state, uri) .await? .ok_or_else(not_found)?; let thing = thing_view(state, row, viewer_did.clone()).await?; let models = models_for_thing(state, uri, viewer_did).await?; Ok((thing, models))}
async fn models_for_thing( state: &AppState, thing_uri: &str, viewer_did: Option<Did<&str>>,) -> AppResult<Vec<ModelView>> { let rows = db( sqlx::query_as!( ModelRow, r#"SELECT m.did AS "did!", m.rkey AS "rkey!", m.uri AS "uri!", m.cid AS "cid!", m.name AS "name!", m.summary, m.created_at AS "created_at!", m.indexed_at AS "indexed_at!", m.record_json, (SELECT COUNT(*) FROM model_parts mp WHERE mp.model_uri = m.uri) AS "part_count!: i64" FROM thing_models tm JOIN models m ON m.uri = tm.model_uri WHERE tm.thing_uri = ? ORDER BY tm.position"#, thing_uri, ) .fetch_all(&state.pool) .await, )?; let mut views = Vec::with_capacity(rows.len()); for row in rows { views.push(model_view(state, row, viewer_did.clone()).await?); } Ok(views)}
pub(crate) struct ModelDetail { pub model: ModelView, pub parts: Vec<PartView>, pub thing: Option<ThingViewBasic>,}
pub(crate) async fn get_model( state: &AppState, uri: &str, viewer_did: Option<Did<&str>>,) -> AppResult<ModelDetail> { let row = db( sqlx::query_as!( ModelRow, r#"SELECT m.did AS "did!", m.rkey AS "rkey!", m.uri AS "uri!", m.cid AS "cid!", m.name AS "name!", m.summary, m.created_at AS "created_at!", m.indexed_at AS "indexed_at!", m.record_json, (SELECT COUNT(*) FROM model_parts mp WHERE mp.model_uri = m.uri) AS "part_count!: i64" FROM models m WHERE m.uri = ?"#, uri, ) .fetch_optional(&state.pool) .await, )? .ok_or_else(not_found)?; let model = model_view(state, row, viewer_did.clone()).await?;
let parts = db(sqlx::query_as!( PartRow, r#"SELECT p.did AS "did!", p.rkey AS "rkey!", p.uri AS "uri!", p.cid AS "cid!", p.name AS "name!", p.format, p.file_json AS "file_json!", p.created_at AS "created_at!", p.indexed_at AS "indexed_at!", p.record_json FROM model_parts mp JOIN parts p ON p.uri = mp.part_uri WHERE mp.model_uri = ? ORDER BY mp.position"#, uri, ) .fetch_all(&state.pool) .await)?; let mut part_views = Vec::with_capacity(parts.len()); for row in parts { part_views.push(part_view(state, row, viewer_did.clone()).await?); }
let thing = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm2 WHERE tm2.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm2 JOIN model_parts mp ON mp.model_uri = tm2.model_uri WHERE tm2.thing_uri = t.uri) AS "part_count!: i64" FROM thing_models tm JOIN things t ON t.uri = tm.thing_uri LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE tm.model_uri = ? ORDER BY tm.position LIMIT 1"#, uri, ) .fetch_optional(&state.pool) .await, )?; let thing = if let Some(row) = thing { Some(thing_basic_view(state, row, viewer_did).await?) } else { None };
Ok(ModelDetail { model, parts: part_views, thing, })}
// ---------------------------------------------------------------------------// feeds (feedView of thingViewBasic)// ---------------------------------------------------------------------------
async fn feed_view( state: &AppState, rows: Vec<ThingRow>, cursor: Option<String>, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let mut items = Vec::with_capacity(rows.len()); for row in rows { items.push(FeedItem { thing: thing_basic_view(state, row, viewer_did.clone()).await?, extra_data: None, }); } Ok(FeedView { cursor: cursor.map(s), items, extra_data: None, })}
pub(super) fn empty_feed() -> FeedView { FeedView { cursor: None, items: Vec::new(), extra_data: None, }}
pub(super) async fn author_things( state: &AppState, did: &str, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let fetch = limit + 1; let rows = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE t.did = ? AND (? IS NULL OR t.rkey < ?) ORDER BY t.rkey DESC LIMIT ?"#, did, cursor, cursor, fetch, ) .fetch_all(&state.pool) .await, )?; rkey_paged_feed(state, rows, limit, viewer_did).await}
pub(crate) async fn feed_recent( state: &AppState, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let fetch = limit + 1; let rows = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE (? IS NULL OR t.rkey < ?) ORDER BY t.rkey DESC LIMIT ?"#, cursor, cursor, fetch, ) .fetch_all(&state.pool) .await, )?; rkey_paged_feed(state, rows, limit, viewer_did).await}
pub(crate) async fn feed_following( state: &AppState, viewer_did: Did<&str>, limit: i64, cursor: Option<&str>,) -> AppResult<FeedView> { let cursor = parse_following_cursor(cursor)?; let fetch = limit + 1; let rows = db(sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t JOIN follows f ON f.followed_did = t.did LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE f.follower_did = ? AND (? IS NULL OR t.rkey < ? OR (t.rkey = ? AND t.did < ?)) ORDER BY t.rkey DESC, t.did DESC LIMIT ?"#, viewer_did.as_str(), cursor.as_ref().map(|c| c.rkey.as_str()), cursor.as_ref().map(|c| c.rkey.as_str()), cursor.as_ref().map(|c| c.rkey.as_str()), cursor.as_ref().map(|c| c.did.as_str()), fetch, ) .fetch_all(&state.pool) .await)?; compound_paged_feed(state, rows, limit, Some(viewer_did)).await}
#[derive(Clone, Debug, PartialEq, Eq)]struct FollowingCursor { rkey: String, did: String,}
fn parse_following_cursor(cursor: Option<&str>) -> AppResult<Option<FollowingCursor>> { let Some(cursor) = cursor else { return Ok(None); }; let Some(rest) = cursor.strip_prefix("following:v1:") else { return Err(invalid_request("invalid following cursor")); }; let Some((rkey, did)) = rest.split_once(':') else { return Err(invalid_request("invalid following cursor")); }; if rkey.is_empty() || did.is_empty() { return Err(invalid_request("invalid following cursor")); } Ok(Some(FollowingCursor { rkey: rkey.to_string(), did: did.to_string(), }))}
fn following_cursor(row: &ThingRow) -> String { format!("following:v1:{}:{}", row.rkey, row.did)}
/// Reduce an over-fetched (`limit+1`) rkey-ordered page into a feed, returning/// the last *returned* item's `rkey` as the cursor when another page exists./// Using the last returned item (not the over-fetched boundary) keeps/// `rkey < cursor` from skipping the boundary item on the next page.async fn rkey_paged_feed( state: &AppState, mut rows: Vec<ThingRow>, limit: i64, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let cursor = if rows.len() as i64 > limit { rows.get((limit - 1) as usize).map(|r| r.rkey.clone()) } else { None }; rows.truncate(limit as usize); feed_view(state, rows, cursor, viewer_did).await}
async fn compound_paged_feed( state: &AppState, mut rows: Vec<ThingRow>, limit: i64, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let cursor = if rows.len() as i64 > limit { rows.get((limit - 1) as usize).map(following_cursor) } else { None }; rows.truncate(limit as usize); feed_view(state, rows, cursor, viewer_did).await}
pub(super) async fn feed_hot( state: &AppState, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let default_now_nanos = chrono::Utc::now() .timestamp_nanos_opt() .expect("current timestamp must fit in i64 nanoseconds"); feed_hot_at(state, limit, cursor, viewer_did, default_now_nanos).await}
pub(super) async fn feed_hot_at( state: &AppState, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>, default_now_nanos: i64,) -> AppResult<FeedView> { let page_state = parse_hot_cursor(cursor, default_now_nanos)?; let now = page_state.now_nanos; let mut scored = hot_positive_candidates(state, limit, &page_state).await?;
if scored.len() as i64 <= limit { scored.extend( hot_zero_engagement_candidates(state, limit + 1 - scored.len() as i64, &page_state) .await?, ); }
// score desc, then rkey desc and uri desc. rkeys are only per-repo unique, so // uri completes the total order used by the page boundary. sort_hot_scores(&mut scored); scored.truncate(limit as usize + 1); let cursor = if scored.len() as i64 > limit { scored .get((limit - 1) as usize) .map(|(score, row)| encode_hot_cursor(now, *score, &row.rkey, &row.uri)) } else { None }; scored.truncate(limit as usize); let page = scored.into_iter().map(|(_, row)| row).collect(); feed_view(state, page, cursor, viewer_did).await}
async fn hot_positive_candidates( state: &AppState, limit: i64, page_state: &HotPageState,) -> AppResult<Vec<(f64, ThingRow)>> { if !page_state.may_include_positive_scores() { return Ok(Vec::new()); }
let mut scored = Vec::new(); let mut offset = 0; loop { // PM-51 keeps ranking exact without a fixed recent window, but this is // still a per-request candidate scan. PM-56 tracks replacing it with a // materialized/indexed hot-feed ranking table. let rows = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE COALESCE(cs.like_count, 0) + COALESCE(cs.save_count, 0) > 0 ORDER BY COALESCE(cs.like_count, 0) + COALESCE(cs.save_count, 0) DESC, t.rkey DESC, t.uri DESC LIMIT ? OFFSET ?"#, HOT_POSITIVE_BATCH, offset, ) .fetch_all(&state.pool) .await, )?; if rows.is_empty() { break; }
let batch_len = rows.len() as i64; let last_engagement = rows .last() .map(|row| row.like_count + row.save_count) .unwrap_or(0); for row in rows { let score = hot_score(&row, page_state.now_nanos); if page_state.is_after_boundary(score, &row.rkey, &row.uri) { scored.push((score, row)); } } sort_hot_scores(&mut scored); scored.truncate(limit as usize + 1);
if batch_len < HOT_POSITIVE_BATCH || can_stop_positive_scan(&scored, limit, last_engagement) { break; } offset += HOT_POSITIVE_BATCH; } Ok(scored)}
async fn hot_zero_engagement_candidates( state: &AppState, fetch: i64, page_state: &HotPageState,) -> AppResult<Vec<(f64, ThingRow)>> { if fetch <= 0 || !page_state.may_include_zero_scores() { return Ok(Vec::new()); } let (after_rkey, after_uri) = page_state.zero_score_boundary(); let rows = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things t LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE COALESCE(cs.like_count, 0) + COALESCE(cs.save_count, 0) = 0 AND (? IS NULL OR t.rkey < ? OR (t.rkey = ? AND t.uri < ?)) ORDER BY t.rkey DESC, t.uri DESC LIMIT ?"#, after_rkey, after_rkey, after_rkey, after_uri, fetch, ) .fetch_all(&state.pool) .await, )?; Ok(rows.into_iter().map(|row| (0.0, row)).collect())}
fn sort_hot_scores(scored: &mut [(f64, ThingRow)]) { scored.sort_by(hot_score_order);}
fn hot_score_order(a: &(f64, ThingRow), b: &(f64, ThingRow)) -> Ordering { b.0.partial_cmp(&a.0) .unwrap_or(Ordering::Equal) .then_with(|| b.1.rkey.cmp(&a.1.rkey)) .then_with(|| b.1.uri.cmp(&a.1.uri))}
fn can_stop_positive_scan(scored: &[(f64, ThingRow)], limit: i64, last_engagement: i64) -> bool { let Some((worst_score, _)) = scored.get(limit as usize) else { return false; }; let max_unseen_score = (last_engagement as f64) / 2.0_f64.powf(1.5); max_unseen_score < *worst_score}
struct HotPageState { now_nanos: i64, boundary: Option<HotCursorBoundary>,}
struct HotCursorBoundary { score: f64, rkey: String, uri: String,}
impl HotPageState { fn may_include_positive_scores(&self) -> bool { self.boundary .as_ref() .is_none_or(|boundary| boundary.score > 0.0) }
fn may_include_zero_scores(&self) -> bool { self.boundary .as_ref() .is_none_or(|boundary| boundary.score >= 0.0) }
fn zero_score_boundary(&self) -> (Option<&str>, Option<&str>) { match &self.boundary { Some(boundary) if boundary.score == 0.0 => { (Some(boundary.rkey.as_str()), Some(boundary.uri.as_str())) } _ => (None, None), } }
fn is_after_boundary(&self, score: f64, rkey: &str, uri: &str) -> bool { let Some(boundary) = &self.boundary else { return true; }; score < boundary.score || (score == boundary.score && (rkey < boundary.rkey.as_str() || (rkey == boundary.rkey.as_str() && uri < boundary.uri.as_str()))) }}
fn parse_hot_cursor(cursor: Option<&str>, default_now_nanos: i64) -> AppResult<HotPageState> { let Some(cursor) = cursor else { return Ok(HotPageState { now_nanos: default_now_nanos, boundary: None, }); };
// The hot cursor pins the ranking timestamp. Without that snapshot value, // time-dependent hot scores can shift between requests and make a page // boundary skip or duplicate rows. let mut parts = cursor.splitn(6, ':'); let valid_prefix = parts.next() == Some("hot") && parts.next() == Some("v1"); let Some(now_nanos) = parts.next() else { return Err(invalid_hot_cursor()); }; let Some(score_bits_hex) = parts.next() else { return Err(invalid_hot_cursor()); }; let Some(rkey) = parts.next() else { return Err(invalid_hot_cursor()); }; let Some(uri) = parts.next() else { return Err(invalid_hot_cursor()); }; if !valid_prefix || rkey.is_empty() || uri.is_empty() { return Err(invalid_hot_cursor()); } let now_nanos = now_nanos.parse().map_err(|_| invalid_hot_cursor())?; let score_bits = u64::from_str_radix(score_bits_hex, 16).map_err(|_| invalid_hot_cursor())?; let score = f64::from_bits(score_bits); if !(score.is_finite() && score >= 0.0) { return Err(invalid_hot_cursor()); } Ok(HotPageState { now_nanos, boundary: Some(HotCursorBoundary { score, rkey: rkey.to_owned(), uri: uri.to_owned(), }), })}
fn encode_hot_cursor(now_nanos: i64, score: f64, rkey: &str, uri: &str) -> String { format!("hot:v1:{now_nanos}:{:016x}:{rkey}:{uri}", score.to_bits())}
fn invalid_hot_cursor() -> AppError { invalid_request("cursor must be an opaque hot feed cursor")}
fn hot_score(row: &ThingRow, now_nanos: i64) -> f64 { let age_hours = ((now_nanos - row.created_at).max(0) as f64) / 3_600_000_000_000.0; let engagement = row.like_count as f64 + row.save_count as f64; engagement / (age_hours + 2.0).powf(1.5)}
pub(super) async fn search_things( state: &AppState, q: &str, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>,) -> AppResult<FeedView> { let offset = parse_offset(cursor)?; let fetch = limit + 1; let rows = db( sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM things_fts JOIN things t ON t.rowid = things_fts.rowid LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE things_fts MATCH ? ORDER BY bm25(things_fts, 10.0, 5.0, 3.0, 1.0) LIMIT ? OFFSET ?"#, q, fetch, offset, ) .fetch_all(&state.pool) .await, )?; let cursor = offset_page_cursor(rows.len(), limit, offset); let page: Vec<ThingRow> = rows.into_iter().take(limit as usize).collect(); feed_view(state, page, cursor, viewer_did).await}
fn parse_offset(cursor: Option<&str>) -> AppResult<i64> { match cursor { None => Ok(0), Some(c) => { let offset: i64 = c .parse() .map_err(|_| invalid_request("cursor must be a non-negative integer offset"))?; if offset < 0 { return Err(invalid_request( "cursor must be a non-negative integer offset", )); } Ok(offset) } }}
fn offset_page_cursor(row_count: usize, limit: i64, offset: i64) -> Option<String> { (row_count as i64 > limit).then(|| (offset + limit).to_string())}
// ---------------------------------------------------------------------------// getList (thingViewBasic[] from listitems)// ---------------------------------------------------------------------------
pub(super) async fn list_things( state: &AppState, list_uri: &str, limit: i64, cursor: Option<&str>, viewer_did: Option<Did<&str>>,) -> AppResult<(Vec<ThingViewBasic>, Option<String>)> { let offset = parse_offset(cursor)?; let fetch = limit + 1; let rows = db(sqlx::query_as!( ThingRow, r#"SELECT t.did AS "did!", t.rkey AS "rkey!", t.uri AS "uri!", t.cid AS "cid!", t.name AS "name!", t.summary, t.license AS "license!", t.tags_json, t.cover_json, t.derived_from_uri, t.created_at AS "created_at!", t.indexed_at AS "indexed_at!", t.record_json, COALESCE(cs.like_count, 0) AS "like_count!: i64", COALESCE(cs.save_count, 0) AS "save_count!: i64", (SELECT COUNT(*) FROM thing_models tm WHERE tm.thing_uri = t.uri) AS "model_count!: i64", (SELECT COUNT(*) FROM thing_models tm JOIN model_parts mp ON mp.model_uri = tm.model_uri WHERE tm.thing_uri = t.uri) AS "part_count!: i64" FROM listitems li JOIN things t ON t.uri = li.subject_uri LEFT JOIN content_stats cs ON cs.uri = t.uri WHERE li.list_uri = ? ORDER BY li.created_at, li.did, li.rkey LIMIT ? OFFSET ?"#, list_uri, fetch, offset, ) .fetch_all(&state.pool) .await)?; let cursor = offset_page_cursor(rows.len(), limit, offset); let mut items = Vec::with_capacity(rows.len().min(limit as usize)); for row in rows.into_iter().take(limit as usize) { items.push(thing_basic_view(state, row, viewer_did.clone()).await?); } Ok((items, cursor))}
// ---------------------------------------------------------------------------// getProfile (polymodel variant)// ---------------------------------------------------------------------------
pub(super) struct ProfileStats { pub thing_count: i64, pub total_likes: i64,}
#[derive(sqlx::FromRow)]struct ProfileListRow { uri: String, cid: String, name: String, description: Option<String>, purpose: Option<String>, created_at: i64, item_count: i64,}
async fn profile_for_did(state: &AppState, did: &str) -> AppResult<Option<ProfileRow>> { let row = db(sqlx::query_as!( ProfileRow, r#"SELECT did AS "did!", display_name, description, avatar_json, default_license, pronouns, printers_json, links_json, record_json AS "record_json!", indexed_at AS "indexed_at!" FROM profiles WHERE did = ?"#, did, ) .fetch_optional(&state.pool) .await)?; Ok(row)}
pub(super) async fn profile_stats(state: &AppState, did: &str) -> AppResult<ProfileStats> { #[derive(sqlx::FromRow)] struct StatsRow { thing_count: i64, total_likes: i64, } let row = db( sqlx::query_as!( StatsRow, r#"SELECT COUNT(*) AS "thing_count!: i64", COALESCE((SELECT SUM(cs.like_count) FROM things t2 LEFT JOIN content_stats cs ON cs.uri = t2.uri WHERE t2.did = ?), 0) AS "total_likes!: i64" FROM things t WHERE t.did = ?"#, did, did, ) .fetch_one(&state.pool) .await, )?; Ok(ProfileStats { thing_count: row.thing_count, total_likes: row.total_likes, })}
async fn profile_lists(state: &AppState, did: &str) -> AppResult<Vec<ProfileListView>> { let rows = db(sqlx::query_as!( ProfileListRow, r#"SELECT l.uri AS "uri!", l.cid AS "cid!", l.name AS "name!", l.description, l.purpose, l.created_at AS "created_at!", COUNT(li.subject_uri) AS "item_count!: i64" FROM lists l LEFT JOIN listitems li ON li.list_uri = l.uri WHERE l.did = ? GROUP BY l.uri, l.cid, l.name, l.description, l.purpose, l.created_at ORDER BY l.created_at DESC, l.rkey DESC"#, did, ) .fetch_all(&state.pool) .await)?;
rows.into_iter() .map(|row| { Ok(ProfileListView { uri: at_uri(&row.uri)?, cid: jacquard_common::types::string::Cid::new_owned(row.cid.as_bytes()) .map_err(|e| internal(format!("invalid list cid: {e}")))?, name: s(row.name), description: row.description.map(s), purpose: row.purpose.map(s), item_count: row.item_count, created_at: datetime_nanos(row.created_at), extra_data: None, }) }) .collect()}
pub(super) async fn handle_for_did(state: &AppState, did: &Did) -> AppResult<Option<String>> { let did = did.as_str(); #[derive(sqlx::FromRow)] struct HandleRow { handle: Option<String>, } let row = db(sqlx::query_as!( HandleRow, r#"SELECT handle AS "handle?" FROM identities WHERE did = ?"#, did ) .fetch_optional(&state.pool) .await)?; Ok(row.and_then(|r| r.handle))}
/// Resolve an actor's handle from the identity projection, falling back to the/// DID document if no identity event has landed yet. A polymodel-profiled actor/// always has a resolvable handle — never return an empty string, which would/// fail handle validation.pub(super) async fn resolve_handle(state: &AppState, did: &Did) -> AppResult<String> { if let Some(h) = handle_for_did(state, did).await? { return Ok(h); } let doc = state .resolver .resolve_did_doc(did) .await .map_err(|e| internal(format!("DID document resolution failed: {e}")))? .into_owned() .map_err(|e| internal(format!("DID document parse failed: {e}")))?; doc.also_known_as .and_then(|aka| aka.into_iter().next()) .map(|h| h.to_string()) .ok_or_else(|| internal("DID document has no handle (alsoKnownAs)"))}
/// Build the polymodel `profileView` union variant for a DID, or `None` if the/// actor has no `space.polymodel.actor.profile` record.pub(super) async fn polymodel_profile_view( state: &AppState, did: &Did,) -> AppResult<Option<polymodel_api::space_polymodel::actor::ProfileView>> { let Some(row) = profile_for_did(state, did.as_ref()).await? else { return Ok(None); }; let stats = profile_stats(state, did.as_ref()).await?; let follows = actor_follow_stats(&state.pool, did.as_ref(), None).await?; let lists = profile_lists(state, did.as_ref()).await?; let handle_str = resolve_handle(state, did).await?; let record: Data = serde_json::from_str(&row.record_json) .map_err(|e| internal(format!("profile record_json parse: {e}")))?; Ok(Some(polymodel_api::space_polymodel::actor::ProfileView { did: did.clone(), handle: s(handle_str), record, display_name: row.display_name.map(s), description: row.description.map(s), avatar: avatar_url(did.as_ref(), row.avatar_json.as_deref())?, default_license: row.default_license.map(s), pronouns: row.pronouns.map(s), printers: smol_vec(row.printers_json.as_deref()), links: uri_vec(row.links_json.as_deref()), thing_count: Some(stats.thing_count), total_likes: Some(stats.total_likes), follower_count: Some(follows.follower_count), following_count: Some(follows.following_count), viewer: Some(follows.viewer), lists: if lists.is_empty() { None } else { Some(lists) }, indexed_at: Some(datetime_nanos(row.indexed_at)), extra_data: None, }))}
#[cfg(test)]mod hot_tests { use super::*;
fn row(rkey: &str, uri: &str) -> ThingRow { ThingRow { did: "did:plc:test".to_owned(), rkey: rkey.to_owned(), uri: uri.to_owned(), cid: "cid".to_owned(), name: "test".to_owned(), summary: None, license: "CC-BY-4.0".to_owned(), tags_json: None, cover_json: None, derived_from_uri: None, created_at: 0, indexed_at: 0, record_json: None, like_count: 0, save_count: 0, model_count: 0, part_count: 0, } }
#[test] fn positive_scan_bound_stops_only_when_unseen_scores_cannot_enter_page() { let page = vec![ (10.0, row("3c", "at://did:plc:c/thing/3c")), (9.0, row("3b", "at://did:plc:b/thing/3b")), ]; assert!(can_stop_positive_scan(&page, 1, 1)); assert!(!can_stop_positive_scan(&page, 1, 100)); assert!(!can_stop_positive_scan(&page[..1], 1, 1)); }
#[test] fn rejects_malformed_hot_cursor_score_bits() { let bits = |s: f64| format!("{:016x}", s.to_bits()); let cursor = |score_bits: String| format!("hot:v1:100:{score_bits}:rkey:at://did:plc:x/x"); assert!(parse_hot_cursor(Some(&cursor(bits(f64::NAN))), 0).is_err()); assert!(parse_hot_cursor(Some(&cursor(bits(f64::INFINITY))), 0).is_err()); assert!(parse_hot_cursor(Some(&cursor(bits(f64::NEG_INFINITY))), 0).is_err()); assert!(parse_hot_cursor(Some(&cursor(bits(-1.0))), 0).is_err()); assert!(parse_hot_cursor(Some(&cursor(bits(0.0))), 0).is_ok()); assert!(parse_hot_cursor(Some(&cursor(bits(12.5))), 0).is_ok()); }}