Marketplace: cheaper update checks and single-marketplace refreshes (PR review #10)

compute_updates reads each marketplace's head once (snapshot_head, no
snapshot clone) and opens each cache once, sharing trees across installs
through GitTree::at. refresh_marketplaces(Some(id)) re-pins only that
marketplace (mk::set_pins with only) and returns only its snapshot, which
the frontend merges by id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:12:14 -07:00
co-authored by Claude Opus 5.5
parent e62ca8795a
commit c87299dda3
3 changed files with 220 additions and 46 deletions
@@ -568,34 +568,17 @@ async fn snapshot_blocking(
.map_err(|e| format!("Reading the marketplace cache failed: {e}"))
}
/// Make each cache's pin refs exactly the commits installs reference, so a
/// pinned version can never be garbage-collected away. Each marketplace's
/// pins are set under that marketplace's repo lock only (pre-flight F11, PR
/// review #5): a concurrent fetch writes refs in the same repo.
/// Make each cache's pin refs exactly the commits installs reference (see
/// [`mk::set_pins`]), for every marketplace.
pub(crate) async fn refresh_pins(state: &AppState) {
refresh_pins_of(state, None).await;
}
/// [`refresh_pins`] for every marketplace, or only `only`.
async fn refresh_pins_of(state: &AppState, only: Option<&str>) {
let settings = state.settings_store.get();
let pins = mk::pins_by_marketplace(&settings, &state.projects_store.list());
let root = state.marketplace.data_root().to_path_buf();
for m in &settings.marketplaces {
let lock = state.marketplace.repo_lock(&m.id);
let _repo_guard = lock.lock().await;
let repo = git::cache_path(&root, &m.id);
let commits = pins.get(&m.id).cloned().unwrap_or_default();
let id = m.id.clone();
let _ = tokio::task::spawn_blocking(move || {
if !repo.exists() {
return;
}
if let Err(e) = git::set_pins(&repo, &commits) {
log::warn!(
"Could not update the pinned commits of marketplace {}: {}",
id,
e
);
}
})
.await;
}
let projects = state.projects_store.list();
mk::set_pins(&state.marketplace, &settings, &projects, only).await;
}
/// Forget a marketplace's snapshot and delete its cache, under its repo lock.
@@ -640,21 +623,25 @@ pub async fn list_marketplace_snapshots(
.map_err(|e| format!("Reading the marketplace caches failed: {e}"))
}
/// With `marketplace_id`, refreshes (and re-pins) only that marketplace and
/// returns only its snapshot — the frontend merges snapshots by id — or
/// nothing if it was removed meanwhile. Without, refreshes all and returns
/// every snapshot.
#[tauri::command]
pub async fn refresh_marketplaces(
marketplace_id: Option<String>,
state: State<'_, AppState>,
) -> Result<Vec<MarketplaceSnapshot>, String> {
let settings = state.settings_store.get();
let current = || state.settings_store.get();
if let Some(id) = &marketplace_id {
find_marketplace(&settings, id)?;
find_marketplace(&current(), id)?;
let snap = mk::refresh_marketplace(&state.marketplace, &current, id).await;
refresh_pins_of(&state, Some(id)).await;
let still_configured = find_marketplace(&current(), id).is_ok();
return Ok(if still_configured { vec![snap] } else { vec![] });
}
for m in settings
.marketplaces
.iter()
.filter(|m| marketplace_id.as_deref().is_none_or(|id| id == m.id))
{
mk::refresh_marketplace(&state.marketplace, &|| state.settings_store.get(), &m.id).await;
for m in current().marketplaces {
mk::refresh_marketplace(&state.marketplace, &current, &m.id).await;
}
refresh_pins(&state).await;
list_marketplace_snapshots(state).await