From 08e4e5bacc0b5202cd15e8266b33dd852c47c71c Mon Sep 17 00:00:00 2001 From: Timothy Quilling Date: Tue, 9 Dec 2025 15:31:04 -0500 Subject: [PATCH] fix: hydrate viewer state from denormalization --- parakeet/src/hydration/posts/mod.rs | 8 +++--- parakeet/src/loaders/post.rs | 39 +++++++++++++++++++++++------ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/parakeet/src/hydration/posts/mod.rs b/parakeet/src/hydration/posts/mod.rs index 97515e7a..e019ba7a 100644 --- a/parakeet/src/hydration/posts/mod.rs +++ b/parakeet/src/hydration/posts/mod.rs @@ -77,7 +77,7 @@ impl StatefulHydrator<'_> { pub async fn hydrate_post(&self, post: String) -> Option { let (post, threadgate, stats) = self.loaders.posts.load(post).await?; - let viewer = self.get_post_viewer_state(post.post.actor_id, post.post.rkey).await; + let viewer = self.get_post_viewer_state(&post.post).await; let embed = self.hydrate_embed_from_post(&post).await; let author = self.hydrate_profile_basic(post.did.clone()).await?; let threadgate = self.hydrate_threadgate(threadgate, &post.did).await; @@ -379,14 +379,14 @@ impl StatefulHydrator<'_> { .collect() } - async fn get_post_viewer_state(&self, subject_actor_id: i32, subject_rkey: i64) -> Option { + async fn get_post_viewer_state(&self, post: ¶keet_db::models::Post) -> Option { if let Some(viewer_did) = &self.current_actor { // Resolve viewer DID to actor_id using cache let viewer_id = self.loaders.post_state.resolve_viewer(viewer_did).await?; - let data = self.loaders.post_state.get(viewer_id, subject_actor_id, subject_rkey).await?; + let data = self.loaders.post_state.get(viewer_id, post).await?; - Some(build_viewer(viewer_did, viewer_id, subject_actor_id, data)) + Some(build_viewer(viewer_did, viewer_id, post.actor_id, data)) } else { None } diff --git a/parakeet/src/loaders/post.rs b/parakeet/src/loaders/post.rs index f34aedb3..d2ed2bb2 100644 --- a/parakeet/src/loaders/post.rs +++ b/parakeet/src/loaders/post.rs @@ -1320,15 +1320,40 @@ impl PostStateLoader { &self.1 } - /// Get post state using natural keys (optimized - no DID resolution) - pub async fn get(&self, viewer_id: i32, subject_actor_id: i32, subject_rkey: i64) -> Option { + /// Get post state from already-loaded post data (optimized - no posts table query) + /// + /// Computes likes/reposts/postgates in Rust from post arrays. + /// Only queries database for bookmarks and pinned status. + /// + /// # Arguments + /// * `viewer_id` - Already-resolved viewer actor_id + /// * `post` - Already-loaded post with engagement data + pub async fn get(&self, viewer_id: i32, post: ¶keet_db::models::Post) -> Option { + // Build the tuple expected by get_post_states (URI is unused, only needed for return key mapping) + let posts_with_data = vec![( + String::new(), // URI unused internally, only for HashMap key in batch operations + post.actor_id, + post.rkey, + post.cid.clone(), + post.like_actor_ids.clone(), + post.like_rkeys.clone(), + post.repost_actor_ids.clone(), + post.repost_rkeys.clone(), + post.postgate_rules.as_ref().map(|rules| { + rules.0.iter().map(|r| Some(*r)).collect() + }), + )]; + let mut conn = self.0.get().await.unwrap(); + let states_result = db::get_post_states(&mut conn, viewer_id, &posts_with_data).await; - let state_result = db::get_post_state(&mut conn, viewer_id, subject_actor_id, subject_rkey).await; - state_result.unwrap_or_else(|e| { - tracing::error!("post state load failed: {e}"); - None - }) + match states_result { + Ok(mut states) => states.pop(), + Err(e) => { + tracing::error!("post state load failed: {e}"); + None + } + } } /// Resolve viewer DID to actor_id (helper for single-post queries) -- 2.51.2