Rust AppView - highly experimental!
fork

Configure Feed

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

chore: cleanup

+22 -53
+1 -10
parakeet/src/db/posts.rs
··· 148 148 } 149 149 } 150 150 151 - // NOTE: get_post_id_by_uri removed - posts table no longer has id column 152 - // Use natural keys (actor_id, rkey) directly instead 153 - 154 151 /// Batch lookup post IDs from AT URIs 155 152 /// 156 153 /// Returns HashMap mapping AT URI to internal post ID ··· 183 180 return Ok(HashMap::new()); 184 181 } 185 182 186 - // TODO: Update cache to use PostKey instead of i64 187 - // For now, skip cache and query all URIs 183 + // Cache currently uses i64 IDs, querying directly for now 188 184 let mut result = HashMap::new(); 189 185 let uris_to_query = uris.to_vec(); 190 186 ··· 356 352 Some((uri, post_key)) 357 353 }) 358 354 .collect(); 359 - 360 - // TODO: Populate cache with new results (cache needs to be updated to use PostKey) 361 - // if let Some(cache) = cache { 362 - // cache.set_post_ids(&db_results).await; 363 - // } 364 355 365 356 // Return database results 366 357 result.extend(db_results);
+3 -5
parakeet/src/db/search.rs
··· 205 205 tags: &[String], 206 206 since: Option<chrono::NaiveDateTime>, 207 207 until: Option<chrono::NaiveDateTime>, 208 - _sort: &str, // TODO: Implement BM25 ranking for "top" sort 208 + _sort: &str, // TODO: BM25 ranking for "top" sort 209 209 limit: i64, 210 210 cursor: Option<f64>, 211 211 ) -> QueryResult<Vec<PostSearchResult>> { ··· 235 235 micros << 10 236 236 }); 237 237 238 - // For token-based search, "top" sort doesn't have a good relevance metric yet 239 - // So we'll just use chronological order (latest) for both modes 240 - // TODO: Implement BM25 or similar for relevance ranking 238 + // Using chronological order for now - "top" sort requires relevance ranking 241 239 242 240 if has_text { 243 241 // Search with text tokens ··· 359 357 tags: &[String], 360 358 since: Option<chrono::NaiveDateTime>, 361 359 until: Option<chrono::NaiveDateTime>, 362 - _sort: &str, // TODO: Implement BM25 ranking for "top" sort 360 + _sort: &str, // TODO: BM25 ranking for "top" sort 363 361 limit: i64, 364 362 cursor: Option<f64>, 365 363 ) -> QueryResult<Vec<PostSearchResultByIds>> {
+1 -1
parakeet/src/hydration/profile/builders.rs
··· 64 64 ProfileViewerState { 65 65 muted: data.muting.unwrap_or_default(), 66 66 muted_by_list: list_mute, 67 - blocked_by: data.blocked.unwrap_or_default(), // TODO: this doesn't factor for blocklists atm 67 + blocked_by: data.blocked.unwrap_or_default(), // TODO: Include blocklist memberships 68 68 blocking, 69 69 blocking_by_list: list_block, 70 70 following,
+1 -4
parakeet/src/hydration/profile/verification.rs
··· 38 38 // Return None if profile doesn't exist 39 39 let profile = profile.as_ref()?; 40 40 41 - // Why says there's a way for the client to configure which verifiers to trust (explicitly 42 - // mentioning the deer.social client which doesn't run an AppView) - except I can't see any 43 - // parameters or headers in requests, or anywhere in social-app where one would configure that. 44 - // Presuming this means that this will be an option eventually (or I'm missing something) 41 + // Use configured trusted verifiers. 45 42 let accept_verifiers = TRUSTED_VERIFIERS.get().unwrap(); 46 43 let is_trusted_verifier = accept_verifiers.iter().any(|v| v == did); 47 44
+1 -1
parakeet/src/hydration/starter_packs.rs
··· 40 40 record: enriched.record, 41 41 creator, 42 42 list, 43 - list_items_sample: vec![], // TODO: we should do this, but the app seems to be okay without? 43 + list_items_sample: vec![], 44 44 feeds, 45 45 list_item_count, 46 46 joined_week_count: 0,
+1 -2
parakeet/src/loaders/embed.rs
··· 1 1 use serde::{Deserialize, Serialize}; 2 2 3 - // EmbedLoader removed - now using hydrate_embed_from_post() instead 4 - // This file now only contains type definitions used by hydration code 3 + // Type definitions used by embed hydration code 5 4 6 5 // Enriched PostEmbedRecord with reconstructed URI 7 6 #[derive(Debug, Clone, Serialize, Deserialize)]
+1 -1
parakeet/src/loaders/mod.rs
··· 12 12 use diesel_async::pooled_connection::deadpool::Pool; 13 13 use diesel_async::AsyncPgConnection; 14 14 15 - // Re-export public types (EmbedLoader removed - now using hydrate_embed_from_post() instead) 15 + // Re-export public types 16 16 pub use embed::{EmbedLoaderRet, EnrichedPostEmbedRecord, PostEmbedImage, PostEmbedVideo, PostEmbedExt}; 17 17 pub use feed::{EnrichedFeedGen, FeedGenKey, FeedGenLoader, LikeRecordLoader}; 18 18 pub use labeler::{EnrichedLabeler, LabelLoader, LabelServiceLoader, LabelServiceLoaderRet};
+5 -12
parakeet/src/loaders/post.rs
··· 4 4 use parakeet_db::models::{self, array_helpers}; 5 5 use std::collections::HashMap; 6 6 7 - /// Extended Post model with computed fields from the old schema 8 - /// (parent_uri, root_uri, did, cid, at_uri reconstructed from FKs) 7 + /// Post model with computed fields (URIs reconstructed from natural keys) 9 8 #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] 10 9 pub struct HydratedPost { 11 10 pub post: models::Post, ··· 176 175 /// Tests can call this to validate SQL syntax without duplicating the query. 177 176 /// Build SQL query for batch loading posts by natural keys (actor_id, rkey) 178 177 /// 179 - /// OPTIMIZED: No tid_timestamp() function call, no actors JOIN 180 - /// - Timestamp computed in Rust via tid_to_datetime(rkey) 181 - /// - Natural keys (actor_id, rkey) passed directly from parsed URIs 182 - /// - DIDs already available from parsing, no JOIN needed 183 - /// - Uses ANY array for 26x better index usage vs IN subquery (0.7ms vs 5.6ms for 30 posts) 178 + /// Uses natural keys directly avoiding actor table joins. 179 + /// Timestamps computed in Rust via tid_to_datetime(rkey). 184 180 /// 185 181 /// This function is public for testing purposes. 186 182 pub fn build_posts_batch_query() -> &'static str { ··· 969 965 .collect(); 970 966 let post_processing_time = post_processing_start.elapsed().as_secs_f64() * 1000.0; 971 967 972 - // OPTIMIZED: Facets are now loaded inline with posts (no separate query needed) 973 968 // Collect all mention actor IDs from facets to batch lookup DIDs 974 969 let facets_start = std::time::Instant::now(); 975 970 let mut mention_actor_ids: Vec<i32> = Vec::new(); ··· 1030 1025 } 1031 1026 use parakeet_db::models::array_helpers::ThreadgateRuleArray; 1032 1027 1033 - // Generate synthetic CID for threadgate (deterministic based on post natural key) 1034 - // Since we no longer store the real threadgate CID, we create a consistent placeholder 1028 + // Generate deterministic CID for threadgate based on post natural key 1035 1029 let synthetic_cid = parakeet_db::cid_util::post_cid_string(post.actor_id, post.rkey); 1036 1030 1037 - // TODO: Load allowed_lists from threadgate_allowed_lists junction table if needed 1038 1031 let allowed_lists = None; 1039 1032 1040 1033 // Store hidden replies as (actor_id, rkey) pairs - URI construction deferred to hydration ··· 1154 1147 _threadgate_hidden_rkeys, 1155 1148 labels, 1156 1149 )| { 1157 - // OPTIMIZED: Process facets from inline composite fields (no separate query) 1150 + // Process facets from inline composite fields 1158 1151 let facet_vec = vec![facet_1, facet_2, facet_3, facet_4, facet_5, facet_6, facet_7, facet_8]; 1159 1152 let facets: Vec<FacetData> = facet_vec 1160 1153 .into_iter()
+2 -3
parakeet/src/timeline_cache.rs
··· 109 109 let prefix = format!("timeline:{}:", actor_id); 110 110 111 111 // Invalidate all entries matching the prefix 112 - // Note: We can't count deletions with moka's Fn closure API 112 + // Deletion metrics not tracked in current implementation 113 113 drop(self.cache.invalidate_entries_if(move |key, _| { 114 114 key.starts_with(&prefix) 115 115 })); ··· 219 219 let prefix = format!("authorfeed:{}:", actor_id); 220 220 221 221 // Invalidate all entries matching the prefix 222 - // Note: We can't count deletions with moka's Fn closure API 222 + // Deletion metrics not tracked in current implementation 223 223 drop(self.cache.invalidate_entries_if(move |key, _| { 224 224 key.starts_with(&prefix) 225 225 })); ··· 336 336 337 337 // Invalidate 338 338 let _deleted = cache.invalidate_by_actor_id(actor_id).await; 339 - // Note: moka's API doesn't allow counting deletions 340 339 341 340 // Verify cache cleared 342 341 assert!(cache.get(actor_id, None).await.is_none());
+1 -2
parakeet/src/xrpc/app_bsky/feed/feedgen.rs
··· 197 197 .and_then(|c| c.parse::<usize>().ok()) 198 198 .unwrap_or(0); 199 199 200 - // Fetch and rank feeds 201 - // TODO: Consider adding moka cache if this becomes a bottleneck 200 + // TODO: Cache opportunity - feed rankings 202 201 let mut conn = state.pool.get().await?; 203 202 204 203 // Fetch all feedgens ordered by like count (uses idx_feedgens_like_count_desc index)
-3
parakeet/src/xrpc/app_bsky/feed/posts/helpers.rs
··· 14 14 #[expect(dead_code)] 15 15 const FEEDGEN_SERVICE_ID: &str = "#bsky_fg"; 16 16 17 - // Note: embed_type filtering is now done in SQL string interpolation in feed queries 18 - // The old diesel-based embed_type_filter function has been removed 19 - 20 17 pub(super) async fn get_feed_skeleton( 21 18 client: &reqwest::Client, 22 19 feed: &str,
+1 -2
parakeet/src/xrpc/app_bsky/graph/suggestions.rs
··· 42 42 &actor_did, 43 43 ).await?; 44 44 45 - // Compute suggestions 46 - // TODO: Consider adding moka cache if this becomes a bottleneck 45 + // TODO: Cache opportunity - similarity suggestions 47 46 48 47 // Get input actor's follower count for similarity ranking 49 48 let input_stats = state
+4 -7
parakeet/src/xrpc/app_bsky/unspecced/mod.rs
··· 155 155 ) -> XrpcResult<Json<GetSuggestedFeedsResponse>> { 156 156 let limit = query.limit.unwrap_or(50).clamp(1, 100) as usize; 157 157 158 - // Fetch and rank feeds (no caching - recalculated each time) 159 - // TODO: Consider adding moka cache if this becomes a bottleneck 158 + // TODO: Cache opportunity - feed rankings 160 159 let mut conn = state.pool.get().await?; 161 160 162 161 // Fetch all feedgens ordered by like count (uses idx_feedgens_like_count_desc index) ··· 234 233 let limit = query.limit.unwrap_or(25).clamp(1, 100) as usize; 235 234 // Category parameter is accepted but ignored for now 236 235 237 - // Compute global suggestions (no caching - recalculated each time) 238 - // TODO: Consider adding moka cache if this becomes a bottleneck 236 + // TODO: Cache opportunity - actor suggestions 239 237 let mut conn = state.pool.get().await?; 240 238 241 239 // Get top 1000 most-followed DIDs by counting follows in our database ··· 333 331 .and_then(|c| c.parse::<usize>().ok()) 334 332 .unwrap_or(0); 335 333 336 - // Fetch and rank feeds (no caching - recalculated each time) 337 - // TODO: Consider adding moka cache if this becomes a bottleneck 334 + // TODO: Cache opportunity - feed rankings 338 335 let mut conn = state.pool.get().await?; 339 336 340 337 // Fetch all feedgens ordered by like count (uses idx_feedgens_like_count_desc index) ··· 462 459 let limit = query.limit.unwrap_or(25).clamp(1, 100) as usize; 463 460 464 461 // Compute rankings (no caching - recalculated each time) 465 - // TODO: Consider adding moka cache if this becomes a bottleneck 462 + // TODO: Cache opportunity - starter pack rankings 466 463 let mut conn = state.pool.get().await?; 467 464 468 465 // Get all starter packs with their owners