···7878 .transpose()
7979 .map_err(|_| ListReposError::BadCursor)?;
80808181- let (entries, next) = tokio::task::spawn_blocking({
8181+ let (account, next) = tokio::task::spawn_blocking({
8282 let db = db.clone();
8383 move || crate::storage::repo::scan_repos(&db, cursor.as_ref(), limit)
8484 })
8585 .await
8686 .map_err(|_| ListReposError::StorageError)??;
87878888- let repos: Vec<Repo<'static>> = entries
8888+ let repos: Vec<Repo<'static>> = account
8989 .into_iter()
9090- .filter_map(|(did, info, prev)| {
9090+ .filter_map(|crate::storage::Account { did, info, prev }| {
9191 let prev = prev?;
9292 // Parse the stored MST-root CID bytes back into a typed CID.
9393 // Use the Str variant explicitly: jacquard-common's Cid::Ipld
+11-10
src/storage/mod.rs
···99pub mod resync_queue;
10101111pub(crate) use error::{StorageError, StorageResult};
1212+pub(crate) use repo::Account;
12131314// ---------------------------------------------------------------------------
1415// key prefixes
···2223type KeyPrefix = [u8; 3];
23242425/// Main collection index (collection → did). See [`collection_index`].
2525-pub(crate) const PREFIX_RBC: KeyPrefix = *b"rbc";
2626+pub(super) const PREFIX_RBC: KeyPrefix = *b"rbc";
2627/// Reversed collection index (did → collection). See [`collection_index`].
2727-pub(crate) const PREFIX_CBR: KeyPrefix = *b"cbr";
2828+pub(super) const PREFIX_CBR: KeyPrefix = *b"cbr";
2829/// Per-repo state and account status. See [`repo`].
2929-pub(crate) const PREFIX_REPO: KeyPrefix = *b"rep";
3030+pub(super) const PREFIX_REPO: KeyPrefix = *b"rep";
3031/// Per-repo transient sync state (rev + prevData CID). See [`repo`].
3131-pub(crate) const PREFIX_REPO_PREV: KeyPrefix = *b"rev";
3232+pub(super) const PREFIX_REPO_PREV: KeyPrefix = *b"rev";
3233/// Firehose subscription cursor (per relay host). See [`firehose_cursor`].
3333-pub(crate) const PREFIX_SUBSCRIBE_REPOS: KeyPrefix = *b"sub";
3434+pub(super) const PREFIX_SUBSCRIBE_REPOS: KeyPrefix = *b"sub";
3435/// listRepos backfill walk progress (per relay host). See [`backfill_progress`].
3535-pub(crate) const PREFIX_LIST_REPOS: KeyPrefix = *b"lsr";
3636+pub(super) const PREFIX_LIST_REPOS: KeyPrefix = *b"lsr";
3637/// Timestamp-ordered resync work queue. See [`resync_queue`].
3737-pub(crate) const PREFIX_RESYNC_QUEUE: KeyPrefix = *b"rsq";
3838+pub(super) const PREFIX_RESYNC_QUEUE: KeyPrefix = *b"rsq";
3839/// Per-repo buffered firehose events during resync. See [`resync_buffer`].
3939-pub(crate) const PREFIX_RESYNC_BUFFER: KeyPrefix = *b"rsb";
4040+pub(super) const PREFIX_RESYNC_BUFFER: KeyPrefix = *b"rsb";
4041/// Per-PDS host state (sync1.1 mode, trust, listRepos cursor/done). See [`pds_host`].
4141-pub(crate) const PREFIX_PDS_HOST: KeyPrefix = *b"pdh";
4242+pub(super) const PREFIX_PDS_HOST: KeyPrefix = *b"pdh";
4243/// listHosts walk cursor (per upstream relay host). See [`list_hosts_cursor`].
4343-pub(crate) const PREFIX_LIST_HOSTS: KeyPrefix = *b"lhs";
4444+pub(super) const PREFIX_LIST_HOSTS: KeyPrefix = *b"lhs";
44454546use std::path::Path;
4647use std::sync::Arc;
+14-26
src/storage/repo.rs
···400400 Ok((dids, next))
401401}
402402403403+/// full combined details about an account
404404+pub struct Account {
405405+ pub did: Did<'static>,
406406+ pub info: RepoInfo,
407407+ pub prev: Option<RepoPrev>,
408408+}
409409+403410/// Iterate over repos in the `rep` keyspace, starting at `cursor` (inclusive).
404411///
405412/// Returns at most `limit` entries. `next` is the first DID of the next page,
···411418 db: &DbRef,
412419 cursor: Option<&Did<'_>>,
413420 limit: usize,
414414-) -> StorageResult<(
415415- Vec<(Did<'static>, RepoInfo, Option<RepoPrev>)>,
416416- Option<Did<'static>>,
417417-)> {
418418- // TODO: use fjall prefix_range
419419- // TODO: probably snapshot so we get a consistent account view?
420420-421421- let prefix_len = PREFIX_REPO.len();
421421+) -> StorageResult<(Vec<Account>, Option<Did<'static>>)> {
422422+ let snapshot = db.database.snapshot();
422423423423- let start_key: Vec<u8> = {
424424- let mut k = PREFIX_REPO.to_vec();
425425- if let Some(did) = cursor {
426426- k.extend_from_slice(did.as_str().as_bytes());
427427- }
428428- k
429429- };
424424+ let start_suffix: Vec<u8> = cursor.map(|did| did.as_bytes().to_vec()).unwrap_or(vec![]);
425425+ let mut ranger = snapshot.range(&db.ks, prefixed_range(PREFIX_REPO, start_suffix..));
430426431431- let mut ranger = db.ks.range(start_key..);
432427 let mut entries = Vec::with_capacity(limit);
433433-434428 for guard in ranger.by_ref() {
435429 let (k, v) = guard.into_inner()?;
436436- if !k.starts_with(&PREFIX_REPO) {
437437- break;
438438- }
439439- let did_str = std::str::from_utf8(&k[prefix_len..]).map_err(|_| StorageError::Corrupt {
430430+ let did_str = std::str::from_utf8(&k[3..]).map_err(|_| StorageError::Corrupt {
440431 key: String::from_utf8_lossy(&k).to_string(),
441432 reason: "non-UTF-8 DID in repo key",
442433 })?;
···455446 decode_repo_prev(&b, &pk_str)
456447 })
457448 .transpose()?;
458458- entries.push((did, info, prev));
449449+ entries.push(Account { did, info, prev });
459450 if entries.len() >= limit {
460451 break;
461452 }
···466457 break None;
467458 };
468459 let key = guard.key()?;
469469- if !key.starts_with(&PREFIX_REPO) {
470470- break None;
471471- }
472472- let did_str = match std::str::from_utf8(&key[prefix_len..]) {
460460+ let did_str = match std::str::from_utf8(&key[3..]) {
473461 Ok(s) => s,
474462 Err(_) => continue,
475463 };
+21-8
src/sync/resync/dispatcher.rs
···3737/// the dispatcher will not dispatch new work to that host.
3838const RATE_LIMIT_COOLDOWN_SECS: u64 = 60;
39394040+pub struct DispatcherConfig {
4141+ pub resolver: std::sync::Arc<crate::identity::Resolver>,
4242+ pub db: DbRef,
4343+ pub client: crate::http::ThrottledClient,
4444+ pub max_concurrent: usize,
4545+ pub describe_timeout: std::time::Duration,
4646+ pub get_repo_timeout: std::time::Duration,
4747+ pub token: tokio_util::sync::CancellationToken,
4848+ pub force_get_repo: bool,
4949+}
5050+4051/// Run the resync dispatcher until the future is cancelled.
4152///
4253/// Polls the resync queue, spawning up to `max_concurrent` worker tasks.
4354/// Workers report their outcome back to the dispatcher, which handles state
4455/// transitions and re-enqueues failed jobs with exponential backoff.
4556pub async fn run(
4646- resolver: std::sync::Arc<crate::identity::Resolver>,
4747- db: DbRef,
4848- client: crate::http::ThrottledClient,
4949- max_concurrent: usize,
5050- describe_timeout: std::time::Duration,
5151- get_repo_timeout: std::time::Duration,
5252- token: tokio_util::sync::CancellationToken,
5353- force_get_repo: bool,
5757+ DispatcherConfig {
5858+ resolver,
5959+ db,
6060+ client,
6161+ max_concurrent,
6262+ describe_timeout,
6363+ get_repo_timeout,
6464+ token,
6565+ force_get_repo,
6666+ }: DispatcherConfig,
5467) -> Result<()> {
5568 let mut busy: HashSet<Did<'static>> = HashSet::new();
5669 let mut task_dids: HashMap<TaskId, Did<'static>> = HashMap::new();
+2
src/sync/resync/mod.rs
···3434 repo::{AccountStatus, RepoInfo, RepoPrev, RepoState},
3535};
36363737+pub use dispatcher::DispatcherConfig;
3838+3739type Result<T> = std::result::Result<T, ResyncError>;
38403941/// Errors that can occur during a resync operation.