···11-//! IdCache helper functions with automatic database fallback
22-//!
33-//! These functions combine IdCache lookups with database queries,
44-//! automatically fetching and caching missing entries.
55-66-use crate::common::errors::{Error, XrpcResult};
77-use diesel::sql_types::{Array, Integer, Text};
88-use diesel_async::pooled_connection::deadpool::Pool;
99-use diesel_async::{AsyncPgConnection, RunQueryDsl};
1010-use parakeet_db::id_cache::{CachedActor, CachedActorData, IdCache};
1111-use std::collections::HashMap;
1212-use std::sync::Arc;
1313-1414-/// Get actor_id for a DID, fetching from database if not cached
1515-///
1616-/// This automatically:
1717-/// 1. Checks IdCache for the DID
1818-/// 2. If miss, queries database
1919-/// 3. Updates cache with result
2020-/// 4. Returns the actor_id
2121-///
2222-/// # Errors
2323-/// Returns error if DID not found in database
2424-pub async fn get_actor_id_or_fetch(
2525- pool: &Pool<AsyncPgConnection>,
2626- id_cache: &Arc<IdCache>,
2727- did: &str,
2828-) -> XrpcResult<i32> {
2929- // Try cache first
3030- if let Some(actor_id) = id_cache.get_actor_id_only(did).await {
3131- return Ok(actor_id);
3232- }
3333-3434- // Cache miss - query database
3535- let mut conn = pool.get().await.map_err(|e| {
3636- Error::new(
3737- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
3838- "DatabaseError",
3939- Some(format!("Failed to get database connection: {}", e)),
4040- )
4141- })?;
4242-4343- #[derive(diesel::QueryableByName)]
4444- struct ActorRow {
4545- #[diesel(sql_type = Integer)]
4646- id: i32,
4747- #[diesel(sql_type = diesel::sql_types::Nullable<Text>)]
4848- handle: Option<String>,
4949- }
5050-5151- let actor: ActorRow = diesel::sql_query(
5252- "SELECT id, handle
5353- FROM actors
5454- WHERE did = $1"
5555- )
5656- .bind::<Text, _>(did)
5757- .get_result(&mut conn)
5858- .await
5959- .map_err(|e| match e {
6060- diesel::result::Error::NotFound => {
6161- Error::new(
6262- axum::http::StatusCode::NOT_FOUND,
6363- "ActorNotFound",
6464- Some(format!("Actor not found: {}", did)),
6565- )
6666- }
6767- _ => Error::new(
6868- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
6969- "DatabaseError",
7070- Some(format!("Failed to query actor: {}", e)),
7171- ),
7272- })?;
7373-7474- // Cache both forward and reverse lookups
7575- id_cache.set_actor_id(
7676- did.to_string(),
7777- CachedActor {
7878- actor_id: actor.id,
7979- is_allowlisted: false, // Allowlist concept removed - default to false
8080- },
8181- ).await;
8282-8383- id_cache.set_actor_data(
8484- actor.id,
8585- CachedActorData {
8686- did: did.to_string(),
8787- handle: actor.handle,
8888- },
8989- ).await;
9090-9191- Ok(actor.id)
9292-}
9393-9494-/// Get DIDs for multiple actor_ids, fetching from database for cache misses
9595-///
9696-/// This automatically:
9797-/// 1. Checks IdCache for each actor_id
9898-/// 2. For cache misses, queries database in batch
9999-/// 3. Updates cache with results
100100-/// 4. Returns HashMap of actor_id → DID
101101-///
102102-/// # Returns
103103-/// HashMap with all requested actor_ids (omits IDs not found in database)
104104-pub async fn get_actor_dids_or_fetch(
105105- pool: &Pool<AsyncPgConnection>,
106106- id_cache: &Arc<IdCache>,
107107- actor_ids: &[i32],
108108-) -> XrpcResult<HashMap<i32, String>> {
109109- if actor_ids.is_empty() {
110110- return Ok(HashMap::new());
111111- }
112112-113113- // Try cache first
114114- let cached = id_cache.get_actor_data_many(actor_ids).await;
115115- let mut result: HashMap<i32, String> = cached
116116- .into_iter()
117117- .map(|(id, data)| (id, data.did))
118118- .collect();
119119-120120- // Find cache misses
121121- let missing: Vec<i32> = actor_ids
122122- .iter()
123123- .filter(|id| !result.contains_key(id))
124124- .copied()
125125- .collect();
126126-127127- if missing.is_empty() {
128128- return Ok(result);
129129- }
130130-131131- // Query database for misses
132132- let mut conn = pool.get().await.map_err(|e| {
133133- Error::new(
134134- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
135135- "DatabaseError",
136136- Some(format!("Failed to get database connection: {}", e)),
137137- )
138138- })?;
139139-140140- #[derive(diesel::QueryableByName)]
141141- struct ActorRow {
142142- #[diesel(sql_type = Integer)]
143143- id: i32,
144144- #[diesel(sql_type = Text)]
145145- did: String,
146146- #[diesel(sql_type = diesel::sql_types::Nullable<Text>)]
147147- handle: Option<String>,
148148- }
149149-150150- let db_results: Vec<ActorRow> = diesel::sql_query(
151151- "SELECT id, did, handle
152152- FROM actors
153153- WHERE id = ANY($1)"
154154- )
155155- .bind::<Array<Integer>, _>(&missing)
156156- .load(&mut conn)
157157- .await
158158- .map_err(|e| {
159159- Error::new(
160160- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
161161- "DatabaseError",
162162- Some(format!("Failed to resolve actor DIDs: {}", e)),
163163- )
164164- })?;
165165-166166- // Update cache and result
167167- for row in db_results {
168168- id_cache.set_actor_data(
169169- row.id,
170170- CachedActorData {
171171- did: row.did.clone(),
172172- handle: row.handle,
173173- },
174174- ).await;
175175-176176- result.insert(row.id, row.did);
177177- }
178178-179179- Ok(result)
180180-}
181181-182182-/// Get actor_ids for multiple DIDs, fetching from database for cache misses
183183-///
184184-/// This automatically:
185185-/// 1. Checks IdCache for each DID
186186-/// 2. For cache misses, queries database in batch
187187-/// 3. Updates cache with results
188188-/// 4. Returns HashMap of DID → actor_id
189189-///
190190-/// # Returns
191191-/// HashMap with all requested DIDs (omits DIDs not found in database)
192192-pub async fn get_actor_ids_or_fetch(
193193- pool: &Pool<AsyncPgConnection>,
194194- id_cache: &Arc<IdCache>,
195195- dids: &[String],
196196-) -> XrpcResult<HashMap<String, i32>> {
197197- if dids.is_empty() {
198198- return Ok(HashMap::new());
199199- }
200200-201201- // Try cache first
202202- let cached = id_cache.get_actor_ids(dids).await;
203203- let mut result: HashMap<String, i32> = cached
204204- .into_iter()
205205- .map(|(did, cached)| (did, cached.actor_id))
206206- .collect();
207207-208208- // Find cache misses
209209- let missing: Vec<&str> = dids
210210- .iter()
211211- .filter(|did| !result.contains_key(did.as_str()))
212212- .map(|s| s.as_str())
213213- .collect();
214214-215215- if missing.is_empty() {
216216- return Ok(result);
217217- }
218218-219219- // Query database for misses
220220- let mut conn = pool.get().await.map_err(|e| {
221221- Error::new(
222222- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
223223- "DatabaseError",
224224- Some(format!("Failed to get database connection: {}", e)),
225225- )
226226- })?;
227227-228228- #[derive(diesel::QueryableByName)]
229229- struct ActorRow {
230230- #[diesel(sql_type = Integer)]
231231- id: i32,
232232- #[diesel(sql_type = Text)]
233233- did: String,
234234- #[diesel(sql_type = diesel::sql_types::Nullable<Text>)]
235235- handle: Option<String>,
236236- }
237237-238238- let db_results: Vec<ActorRow> = diesel::sql_query(
239239- "SELECT id, did, handle
240240- FROM actors
241241- WHERE did = ANY($1)"
242242- )
243243- .bind::<Array<Text>, _>(&missing)
244244- .load(&mut conn)
245245- .await
246246- .map_err(|e| {
247247- Error::new(
248248- axum::http::StatusCode::INTERNAL_SERVER_ERROR,
249249- "DatabaseError",
250250- Some(format!("Failed to resolve actor IDs: {}", e)),
251251- )
252252- })?;
253253-254254- // Update cache and result
255255- for row in db_results {
256256- id_cache.set_actor_id(
257257- row.did.clone(),
258258- CachedActor {
259259- actor_id: row.id,
260260- is_allowlisted: false, // Allowlist concept removed - default to false
261261- },
262262- ).await;
263263-264264- id_cache.set_actor_data(
265265- row.id,
266266- CachedActorData {
267267- did: row.did.clone(),
268268- handle: row.handle,
269269- },
270270- ).await;
271271-272272- result.insert(row.did, row.id);
273273- }
274274-275275- Ok(result)
276276-}
-2
parakeet/src/lib.rs
···1010 //! Common infrastructure and utilities shared across the application
11111212 pub mod auth;
1313- pub mod cache_id_helpers;
1413 pub mod cache_listener;
1514 pub mod cache_timeline;
1615 pub mod errors;
···19182019 // Re-export commonly used items
2120 pub use auth::{AtpAcceptLabelers, AtpAuth, JwtVerifier};
2222- pub use cache_id_helpers::{get_actor_id_or_fetch, get_actor_dids_or_fetch, get_actor_ids_or_fetch};
2321 pub use cache_listener::spawn_cache_listener;
2422 pub use cache_timeline::{AuthorFeedCache, TimelineCache};
2523 pub use errors::{Error, XrpcResult};
+10-7
parakeet/src/xrpc/app_bsky/actor.rs
···258258 .into_response());
259259 }
260260261261- // Convert DIDs to actor_ids for efficient profile loading
262262- let did_to_actor_id = crate::common::get_actor_ids_or_fetch(
263263- &state.pool,
264264- &state.id_cache,
265265- &all_dids,
266266- ).await.unwrap_or_default();
261261+ // Convert DIDs to actor_ids for efficient profile loading using ProfileEntity
262262+ let actor_ids = state.profile_entity.resolve_identifiers(&all_dids)
263263+ .await
264264+ .unwrap_or_default();
267265268268- let actor_ids: Vec<i32> = did_to_actor_id.values().copied().collect();
266266+ // Create a mapping for compatibility with existing code
267267+ let did_to_actor_id: std::collections::HashMap<String, i32> = all_dids
268268+ .iter()
269269+ .cloned()
270270+ .zip(actor_ids.iter().copied())
271271+ .collect();
269272270273 // Load profiles to check quality using ProfileEntity
271274 let profiles = state.profile_entity
+10-22
parakeet/src/xrpc/app_bsky/graph/thread_mutes.rs
···1818 use crate::common::errors::Error;
1919 let mut conn = state.pool.get().await?;
20202121- // Resolve authenticated user's actor_id via IdCache
2222- let actor_id = crate::common::get_actor_id_or_fetch(
2323- &state.pool,
2424- &state.id_cache,
2525- &auth.0,
2626- ).await?;
2121+ // Resolve authenticated user's actor_id using ProfileEntity
2222+ let actor_id = state.profile_entity.resolve_identifier(&auth.0).await
2323+ .map_err(|_| Error::actor_not_found(&auth.0))?;
27242825 // Parse thread root URI and resolve to post_id
2926 let parts = form.root.strip_prefix("at://").ok_or_else(|| Error::invalid_request(Some("Invalid AT URI".into())))?
···3936 let rkey_bigint = parakeet_db::tid_util::decode_tid(rkey_str)
4037 .map_err(|_| Error::invalid_request(Some("Invalid TID in root URI".into())))?;
41384242- let root_post_actor_id = crate::common::get_actor_id_or_fetch(
4343- &state.pool,
4444- &state.id_cache,
4545- root_did,
4646- ).await?;
3939+ let root_post_actor_id = state.profile_entity.resolve_identifier(root_did).await
4040+ .map_err(|_| Error::actor_not_found(root_did))?;
47414842 // Append to thread_mutes array (off-protocol, managed directly by AppView)
4943 // Deduplicates based on root_post_actor_id + root_post_rkey
···7872 use crate::common::errors::Error;
7973 let mut conn = state.pool.get().await?;
80748181- // Resolve authenticated user's actor_id via IdCache
8282- let actor_id = crate::common::get_actor_id_or_fetch(
8383- &state.pool,
8484- &state.id_cache,
8585- &auth.0,
8686- ).await?;
7575+ // Resolve authenticated user's actor_id using ProfileEntity
7676+ let actor_id = state.profile_entity.resolve_identifier(&auth.0).await
7777+ .map_err(|_| Error::actor_not_found(&auth.0))?;
87788879 // Parse thread root URI and resolve to post_id
8980 let parts = form.root.strip_prefix("at://").ok_or_else(|| Error::invalid_request(Some("Invalid AT URI".into())))?
···9990 let rkey_bigint = parakeet_db::tid_util::decode_tid(rkey_str)
10091 .map_err(|_| Error::invalid_request(Some("Invalid TID in root URI".into())))?;
10192102102- let root_post_actor_id = crate::common::get_actor_id_or_fetch(
103103- &state.pool,
104104- &state.id_cache,
105105- root_did,
106106- ).await?;
9393+ let root_post_actor_id = state.profile_entity.resolve_identifier(root_did).await
9494+ .map_err(|_| Error::actor_not_found(root_did))?;
1079510896 // Remove from thread_mutes array (off-protocol, managed directly by AppView)
10997 diesel_async::RunQueryDsl::execute(
···2828 let is_authenticated = maybe_auth.is_some();
2929 let (maybe_did, maybe_actor_id) = if let Some(auth) = maybe_auth {
3030 let did = auth.0.clone();
3131- let actor_id = crate::common::get_actor_id_or_fetch(&state.pool, &state.id_cache, &did).await.ok();
3131+ let actor_id = state.profile_entity.resolve_identifier(&did).await.ok();
3232 (Some(did), actor_id)
3333 } else {
3434 (None, None)
+3-6
parakeet/src/xrpc/community_lexicon/bookmarks.rs
···30303131 let limit = query.limit.unwrap_or(50).clamp(1, 100);
32323333- // Resolve DID to actor_id via IdCache
3434- let actor_id = crate::common::get_actor_id_or_fetch(
3535- &state.pool,
3636- &state.id_cache,
3737- &auth.0,
3838- ).await?;
3333+ // Resolve DID to actor_id using ProfileEntity
3434+ let actor_id = state.profile_entity.resolve_identifier(&auth.0).await
3535+ .map_err(|_| crate::common::errors::Error::actor_not_found(&auth.0))?;
39364037 // Note: tags filtering not supported in current schema
4138 if query.tags.is_some() {