···330330 self.cache.get(did)
331331 }
332332333333+ /// Insert a resolved identity into the cache without a network call.
334334+ ///
335335+ /// Used by the dispatcher to warm the cache for discovery-queue items
336336+ /// whose identity was already resolved during backfill. This avoids a
337337+ /// redundant network resolution in the resync worker.
338338+ pub fn insert_cached(
339339+ &self,
340340+ did: Did<'static>,
341341+ pds: Url,
342342+ pubkey: PublicKey<'static>,
343343+ ) -> Arc<CachedIdentity> {
344344+ self.cache.insert(did, pds, pubkey)
345345+ }
346346+333347 /// Evict `did` from the identity cache.
334348 ///
335349 /// Called when a `#identity` firehose event is received, after all
+16-11
src/sync/backfill.rs
···1515use jacquard_api::com_atproto::sync::list_repos::{ListRepos, RepoStatus};
1616use jacquard_common::{
1717 error::ClientErrorKind,
1818- types::string::Did,
1818+ types::{crypto::PublicKey, string::Did},
1919 url::{Host, Url},
2020 {IntoStatic, xrpc::XrpcExt},
2121};
···137137 // Cache hits are free; misses fall back to the listed host.
138138 //
139139 // TODO: ...this is basically redundant with validate_dids now?
140140- let dids_with_hosts: Vec<(Did<'static>, Arc<Url>, RepoState)> = {
140140+ let dids_with_hosts: Vec<(Did<'static>, Arc<Url>, PublicKey<'static>, RepoState)> = {
141141 let mut out = Vec::with_capacity(dids.len());
142142 for (did, account_state) in dids {
143143 let Some(res) = token.run(resolver.resolve(&did)).await else {
144144 return Ok(false); // cancelled
145145 };
146146- let pds_host = match res {
147147- Ok(resolved) => resolved.pds.clone(),
146146+ let resolved = match res {
147147+ Ok(resolved) => resolved,
148148 Err(e) => {
149149 error!(did = %did, error = %e, "failed to resolve host for validated did; skipping");
150150 continue;
151151 }
152152 };
153153- out.push((did, pds_host, account_state));
153153+ out.push((
154154+ did,
155155+ resolved.pds.clone(),
156156+ resolved.pubkey.clone(),
157157+ account_state,
158158+ ));
154159 }
155160 out
156161 };
···167172168173 // Push newly-discovered repos into the in-memory discovery queue.
169174 // Each push may block if the queue is full (backpressure).
170170- for (did, pds) in new_items {
175175+ for (did, pds, pubkey) in new_items {
171176 let Some(_) = token
172172- .run(discovery_queue.push(DiscoveryItem { did, pds }))
177177+ .run(discovery_queue.push(DiscoveryItem { did, pds, pubkey }))
173178 .await
174179 else {
175180 return Ok(false);
···333338// Storage commit
334339// ---------------------------------------------------------------------------
335340336336-type DidWithPds = (Did<'static>, Arc<Url>);
341341+type DidWithPds = (Did<'static>, Arc<Url>, PublicKey<'static>);
337342338343/// Insert newly-seen DIDs into the repo table and persist the backfill cursor.
339344///
···347352fn persist_page(
348353 db: &DbRef,
349354 host: &Host,
350350- items: Vec<(Did<'static>, Arc<Url>, RepoState)>,
355355+ items: Vec<(Did<'static>, Arc<Url>, PublicKey<'static>, RepoState)>,
351356 progress_cursor: String,
352357 authoritative: bool,
353358) -> Result<(u64, u64, Vec<DidWithPds>)> {
354359 let mut new_active: Vec<DidWithPds> = Vec::new();
355360 let mut inactive_count: u64 = 0;
356356- for (did, pds, repo_state) in items {
361361+ for (did, pds, pubkey, repo_state) in items {
357362 if let Some(deactiveated_account_status) = repo_state.to_account_inactive() {
358363 if write_inactive_status(
359364 db,
···368373 let newly_inserted = storage::repo::ensure_repo(db, &did)?;
369374 if newly_inserted {
370375 db.stats.repos_queued_total.fetch_add(1, Ordering::Relaxed);
371371- new_active.push((did, pds));
376376+ new_active.push((did, pds, pubkey));
372377 }
373378 }
374379 }
+21-4
src/sync/discovery_queue.rs
···2424use std::sync::atomic::{AtomicUsize, Ordering};
2525use std::sync::{Arc, Mutex};
26262727+use jacquard_common::types::crypto::PublicKey;
2728use jacquard_common::types::string::Did;
2829use jacquard_common::url::Url;
29303031/// A newly-discovered repo awaiting its initial resync.
3232+///
3333+/// Carries the resolved identity (PDS + pubkey) from backfill so the
3434+/// dispatcher can warm the identity cache without a redundant network call.
3135pub struct DiscoveryItem {
3236 pub did: Did<'static>,
3337 pub pds: Arc<Url>,
3838+ pub pubkey: PublicKey<'static>,
3439}
35403641pub struct DiscoveryQueue {
···5156 /// Ordered list of PDS hosts for round-robin traversal.
5257 hosts: Vec<Arc<Url>>,
5358 /// Per-host FIFO queues of DIDs awaiting resync.
5454- queues: HashMap<Arc<Url>, VecDeque<Did<'static>>>,
5959+ queues: HashMap<Arc<Url>, VecDeque<(Did<'static>, PublicKey<'static>)>>,
5560 /// Round-robin cursor: index into `hosts` for the next pop.
5661 cursor: usize,
5762 /// Total items across all queues.
···7277 self.hosts.push(item.pds.clone());
7378 self.queues.insert(item.pds.clone(), VecDeque::new());
7479 }
7575- self.queues.get_mut(&item.pds).unwrap().push_back(item.did);
8080+ self.queues
8181+ .get_mut(&item.pds)
8282+ .unwrap()
8383+ .push_back((item.did, item.pubkey));
7684 self.len += 1;
7785 }
7886}
···151159 .queues
152160 .get_mut(&host)
153161 .expect("host in ring must have a queue");
154154- let did = queue.pop_front().expect("host in ring must have items");
162162+ let (did, pubkey) = queue.pop_front().expect("host in ring must have items");
155163 let host_empty = queue.is_empty();
156164 inner.len -= 1;
157165 consecutive_skips = 0;
···167175 inner.cursor = idx + 1;
168176 }
169177170170- result.push(DiscoveryItem { did, pds: host });
178178+ result.push(DiscoveryItem {
179179+ did,
180180+ pds: host,
181181+ pubkey,
182182+ });
171183 }
172184173185 let popped = result.len();
···212224 }
213225214226 fn item(did_str: &str, pds_str: &str) -> DiscoveryItem {
227227+ use jacquard_common::types::crypto::KeyCodec;
215228 DiscoveryItem {
216229 did: did(did_str),
217230 pds: url(pds_str),
231231+ pubkey: PublicKey {
232232+ codec: KeyCodec::Ed25519,
233233+ bytes: std::borrow::Cow::Owned(vec![0u8; 32]),
234234+ },
218235 }
219236 }
220237
+6-2
src/sync/resync/dispatcher.rs
···239239 continue;
240240 }
241241242242+ // Warm the identity cache so the worker gets a cache hit
243243+ // instead of a redundant network resolution.
244244+ resolver.insert_cached(did.clone(), (*dq_item.pds).clone(), dq_item.pubkey);
245245+242246 busy.insert(did.clone());
243247 let item = ResyncItem {
244248 did: dq_item.did,
···366370 let host = resolver
367371 .resolve_cached(did)
368372 .and_then(|r| r.pds.host_str().map(str::to_owned))
369369- .unwrap_or_default();
373373+ .unwrap_or_else(|| "(resolving)".to_owned());
370374 BusyEntry {
371375 did: did.to_string(),
372376 host,
···390394 let host = resolver
391395 .resolve_cached(did)
392396 .and_then(|r| r.pds.host_str().map(str::to_owned))
393393- .unwrap_or_default();
397397+ .unwrap_or_else(|| "(resolving)".to_owned());
394398 *counts.entry(host).or_default() += 1;
395399 }
396400 let mut entries: Vec<_> = counts