Marketplace: edit only the marketplace fields of a project (PR review #2)

ProjectsStore gains update_marketplace_fields / update_all_marketplace_fields,
which read-modify-write installs and opt-outs under the store's own lock.
Install, uninstall, update, forget and set_global_item_disabled use them
instead of writing back a whole Project read earlier, so a concurrent
start's status/container_id change is no longer overwritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 13:04:51 -07:00
co-authored by Claude Opus 5.5
parent e805c29c70
commit d51b54774b
2 changed files with 181 additions and 32 deletions
@@ -761,16 +761,12 @@ pub async fn forget_marketplace_installs(
.global_marketplace_installs
.retain(|i| i.marketplace_id != marketplace_id);
state.settings_store.update(settings)?;
for mut p in state.projects_store.list() {
let before = (p.marketplace_installs.len(), p.marketplace_disabled.len());
p.marketplace_installs
.retain(|i| i.marketplace_id != marketplace_id);
p.marketplace_disabled
.retain(|r| r.marketplace_id != marketplace_id);
if (p.marketplace_installs.len(), p.marketplace_disabled.len()) != before {
state.projects_store.update(p)?;
}
}
state
.projects_store
.update_all_marketplace_fields(|installs, disabled| {
installs.retain(|i| i.marketplace_id != marketplace_id);
disabled.retain(|r| r.marketplace_id != marketplace_id);
})?;
refresh_pins(&state).await;
Ok(())
}
@@ -807,9 +803,12 @@ pub async fn install_marketplace_item(
state.settings_store.update(s)?;
}
InstallScope::Project { project_id } => {
let mut p = find_project(&state, &project_id)?;
ops::upsert_install(&mut p.marketplace_installs, inst);
state.projects_store.update(p)?;
state
.projects_store
.update_marketplace_fields(&project_id, |installs, _| {
ops::upsert_install(installs, inst);
Ok(())
})?;
}
}
refresh_pins(&state).await;
@@ -830,19 +829,23 @@ pub async fn uninstall_marketplace_item(
}
state.settings_store.update(s)?;
// An opt-out of an item that is no longer global means nothing.
for mut p in state.projects_store.list() {
if p.marketplace_disabled.contains(&item) {
ops::set_disabled(&mut p.marketplace_disabled, &item, false);
state.projects_store.update(p)?;
}
}
state
.projects_store
.update_all_marketplace_fields(|_, disabled| {
ops::set_disabled(disabled, &item, false)
})?;
}
InstallScope::Project { project_id } => {
let mut p = find_project(&state, &project_id)?;
if !ops::remove_install(&mut p.marketplace_installs, &item) {
return Err(format!("That item is not installed in \"{}\".", p.name));
}
state.projects_store.update(p)?;
let name = find_project(&state, &project_id)?.name;
state
.projects_store
.update_marketplace_fields(&project_id, |installs, _| {
if ops::remove_install(installs, &item) {
Ok(())
} else {
Err(format!("That item is not installed in \"{name}\"."))
}
})?;
}
}
refresh_pins(&state).await;
@@ -857,9 +860,13 @@ pub async fn set_global_item_disabled(
state: State<'_, AppState>,
) -> Result<Project, String> {
validate_item(&item)?;
let mut p = find_project(&state, &project_id)?;
ops::set_disabled(&mut p.marketplace_disabled, &item, disabled);
state.projects_store.update(p)
let ((), saved) = state
.projects_store
.update_marketplace_fields(&project_id, |_, list| {
ops::set_disabled(list, &item, disabled);
Ok(())
})?;
Ok(saved)
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -924,11 +931,16 @@ pub async fn update_marketplace_item(
state.settings_store.update(s)?;
}
InstallScope::Project { project_id } => {
let mut p = find_project(&state, &project_id)?;
if !ops::repin(&mut p.marketplace_installs, &item, &head) {
return Err(format!("That item is not installed in \"{}\".", p.name));
}
state.projects_store.update(p)?;
let name = find_project(&state, &project_id)?.name;
state
.projects_store
.update_marketplace_fields(&project_id, |installs, _| {
if ops::repin(installs, &item, &head) {
Ok(())
} else {
Err(format!("That item is not installed in \"{name}\"."))
}
})?;
}
}
refresh_pins(&state).await;