···107107 rkey: &str,
108108 ) -> Result<Option<RecordRow>, IndexError> {
109109 // FINAL ensures ReplacingMergeTree deduplication is applied
110110+ // Order by event_time first (firehose data wins), then indexed_at as tiebreaker
111111+ // Include deletes so we can return not-found for deleted records
110112 let query = r#"
111111- SELECT cid, record
113113+ SELECT cid, record, operation
112114 FROM raw_records FINAL
113115 WHERE did = ?
114116 AND collection = ?
115117 AND rkey = ?
116116- AND operation != 'delete'
117117- ORDER BY event_time DESC
118118+ ORDER BY event_time DESC, indexed_at DESC
118119 LIMIT 1
119120 "#;
120121···283284pub struct RecordRow {
284285 pub cid: String,
285286 pub record: String, // JSON string
287287+ pub operation: String,
286288}
287289288290/// Record with rkey from raw_records (for listRecords)
+50-1
crates/weaver-index/src/endpoints/repo.rs
···9696 })?;
97979898 if let Some(row) = cached {
9999+ // Check if record was deleted
100100+ if row.operation == "delete" {
101101+ return Err(XrpcErrorResponse::not_found("Record not found"));
102102+ }
103103+99104 // Cache hit - return from ClickHouse
100105 let value: Data<'_> = serde_json::from_str(&row.record).map_err(|e| {
101106 tracing::error!("Failed to parse record JSON: {}", e);
···103108 })?;
104109105110 let uri_str = format!("at://{}/{}/{}", did, collection, rkey);
106106- let uri = AtUri::new_owned(uri_str).map_err(|e| {
111111+ let uri = AtUri::new_owned(uri_str.clone()).map_err(|e| {
107112 tracing::error!("Failed to construct AT URI: {}", e);
108113 XrpcErrorResponse::internal_error("Failed to construct URI")
109114 })?;
···112117 tracing::error!("Invalid CID in database: {}", e);
113118 XrpcErrorResponse::internal_error("Invalid CID stored")
114119 })?;
120120+121121+ // Stale-while-revalidate: check freshness in background
122122+ let cached_cid = row.cid.clone();
123123+ let clickhouse = state.clickhouse.clone();
124124+ let resolver = state.resolver.clone();
125125+ let did_str = did.as_str().to_string();
126126+ let collection_str = collection.to_string();
127127+ let rkey_str = rkey.to_string();
128128+129129+ tokio::spawn(async move {
130130+ let uri = match AtUri::new_owned(uri_str) {
131131+ Ok(u) => u,
132132+ Err(_) => return,
133133+ };
134134+135135+ let upstream = match resolver.fetch_record_slingshot(&uri).await {
136136+ Ok(r) => r,
137137+ Err(e) => {
138138+ tracing::debug!("Background revalidation fetch failed: {}", e);
139139+ return;
140140+ }
141141+ };
142142+143143+ // Check if CID changed
144144+ let upstream_cid = upstream
145145+ .cid
146146+ .as_ref()
147147+ .map(|c| c.as_str())
148148+ .unwrap_or_default();
149149+150150+ if upstream_cid != cached_cid && !upstream_cid.is_empty() {
151151+ let record_json = serde_json::to_string(&upstream.value).unwrap_or_default();
152152+ if !record_json.is_empty() {
153153+ if let Err(e) = clickhouse
154154+ .insert_record(&did_str, &collection_str, &rkey_str, upstream_cid, &record_json)
155155+ .await
156156+ {
157157+ tracing::warn!("Failed to update stale cache entry: {}", e);
158158+ } else {
159159+ tracing::debug!("Updated stale cache entry for {}", uri);
160160+ }
161161+ }
162162+ }
163163+ });
115164116165 return Ok(Json(
117166 GetRecordOutput {