Marketplace: Tauri commands, store-owned fields and startup refresh
The 21 marketplace commands, registered and granted; marketplace fields kept store-owned in update_settings/update_project; a background refresh of every marketplace at app start. - apply_marketplace_now emits marketplace-sync-finished per project (F4). - Settings export carries marketplace account tokens in ExportedSecrets (account id -> token) and import restores them; imported accounts, marketplaces and global installs are validated with the commands' own rules before anything is written. The import preview discloses the marketplace count, token count and global hook installs, and warns on the latter (F10). - refresh_pins and cache removal hold the repo lock (F11). - ops::validate_host/validate_branch delegate to auth::valid_host and git::valid_branch (F13). - A finished gh container login frees only its own cancel slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -167,6 +167,18 @@ impl MarketplaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Free the slot after a login ends, but only if it still holds that
|
||||
/// login's sender (its receiver is gone once the login returns). A cancel
|
||||
/// may have emptied the slot and a newer login claimed it meanwhile; a
|
||||
/// plain `set_gh_login_cancel(None)` would drop that login's sender,
|
||||
/// which it reads as a cancel.
|
||||
pub async fn release_gh_login(&self) {
|
||||
let mut slot = self.gh_login_cancel.lock().await;
|
||||
if slot.as_ref().is_some_and(|tx| tx.is_closed()) {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn cancel_gh_login(&self) {
|
||||
if let Some(tx) = self.gh_login_cancel.lock().await.take() {
|
||||
let _ = tx.send(());
|
||||
@@ -664,6 +676,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn releasing_a_finished_login_never_frees_a_newer_ones_slot() {
|
||||
let mgr = MarketplaceManager::new(std::env::temp_dir());
|
||||
// Login A is cancelled, and login B claims the slot before A returns.
|
||||
let (tx_a, rx_a) = tokio::sync::oneshot::channel::<()>();
|
||||
assert!(mgr.set_gh_login_cancel(Some(tx_a)).await);
|
||||
mgr.cancel_gh_login().await;
|
||||
let (tx_b, mut rx_b) = tokio::sync::oneshot::channel::<()>();
|
||||
assert!(mgr.set_gh_login_cancel(Some(tx_b)).await);
|
||||
drop(rx_a); // A returns.
|
||||
mgr.release_gh_login().await;
|
||||
assert!(
|
||||
matches!(rx_b.try_recv(), Err(tokio::sync::oneshot::error::TryRecvError::Empty)),
|
||||
"B's sender must still be held, not dropped"
|
||||
);
|
||||
// B returns: its slot is freed.
|
||||
drop(rx_b);
|
||||
mgr.release_gh_login().await;
|
||||
let (tx_c, _rx_c) = tokio::sync::oneshot::channel::<()>();
|
||||
assert!(mgr.set_gh_login_cancel(Some(tx_c)).await);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_project_that_never_had_items_is_not_synced() {
|
||||
let data = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user