6.7 KiB
Push refactor plan
Refactor the grasp push pipeline into SyncState methods with step-by-step
progress the UI can render while a push is in flight.
Motivation
push_staged_to_graspstakes 9 parameters (needs#[allow(clippy::too_many_arguments)]) and mixes three concerns per iteration: signing a Nostr event, relay connection/staging, and git transport.- The flow is orchestrated in two places with different error handling:
Backend::push_repo_fromandannounce_repository_and_pushinsigned_state/src/backend.rs. - The state event is rebuilt and re-signed per server inside the loop, forcing
PushOutcome::keep_newestto exist. - Feedback is binary:
RepoStore::pushingplus a finalResult. The loop runs inside an opaquebackground_spawn, so the UI cannot show where a push is.
Moving signed_core/src/state.rs into signed_state is safe: RepoState is
only used inside signed_state (push.rs, repo.rs, sync_status.rs), and
signed_state already depends on nostr directly. Kind::RepoState references
are the Nostr kind enum and are unaffected.
Target shape
// signed_state/src/sync_state.rs
pub struct SyncState {
pub refs: Vec<(String, String)>,
pub head: Option<String>,
}
pub enum PushStep {
ReadLocalState,
Announce,
StageStateEvent(RelayUrl),
PushGitObjects(RelayUrl),
PublishStateEvent,
}
pub enum StepStatus {
Running,
Done,
Failed(String),
}
pub struct PushProgress {
pub steps: Vec<(PushStep, StepStatus)>,
}
PushProgress is a GPUI Entity the caller creates and passes in; views
observe it and re-render as steps update. Label mapping for the UI:
ReadLocalState-> "Read local refs"Announce-> "Announce repository"StageStateEvent(relay)-> "Publish state event to {relay}"PushGitObjects(relay)-> "Push to grasp {relay}"PublishStateEvent-> "Publish state event on relays"
impl SyncState {
pub fn parse(event: &Event) -> Self;
pub fn build(id: &str, refs: &[(String, String)], head: Option<&str>) -> EventBuilder;
pub fn from_repo(repo: &Repo) -> Result<Self>;
pub async fn push_to_grasps(
&self,
plan: PushPlan,
progress: Entity<PushProgress>,
cx: &mut AsyncApp,
) -> PushOutcome;
async fn stage_on_grasp(...) -> Result<(), String>; // connect + purgatory staging
async fn publish(...); // final broadcast
}
The key enabler: the per-server orchestration loop moves from
background_spawn into the foreground cx.spawn task. Each blocking piece
(ref_state, git push) stays on background threads; network awaits are fine on
the foreground executor. Each step can then update PushProgress directly.
Flow:
UI action
-> Backend push method
-> ReadLocalState (background git)
-> Announce (sign + broadcast, create/publish flows only)
-> for each grasp server:
StageStateEvent(relay) (sign once, send to relay)
PushGitObjects(relay) (background git)
-> PublishStateEvent (broadcast)
-> every step updates Entity<PushProgress> -> views render the checklist
Steps
1. Move and rename (mechanical)
git mv crates/signed_core/src/state.rs crates/signed_state/src/sync_state.rs, renameRepoState->SyncState.signed_core/src/lib.rs: droppub mod state;and the re-export. No deprecated alias; there are no users outsidesigned_state.signed_state/src/lib.rs: addmod sync_state;andpub use sync_state::SyncState;.- Update the three import sites:
push.rs,repo.rs,sync_status.rs. - Tests in the file move along unchanged.
- Validate:
cargo check -p signed_core -p signed_state && cargo test -p signed_state.
2. Consolidate the push core onto SyncState (behavior-preserving)
- Introduce
PushPlan { client, signer, repo_id, owner, servers, path, kind }to kill the 9-arg signature. Replace thepushclosure withenum PushKind { Main, All }- the only two variants used across all call sites (push_mainfor create,push_allotherwise). - Sign the state event once before the server loop instead of per iteration
(BIP-340 signing is deterministic;
created_atis fixed at build time). DeletePushOutcome::keep_newest. A signing failure now aborts the push up front instead of "retrying" per server; key errors are not transient. - Split the loop body into named methods:
SyncState::stage_on_grasp(connect- staging match block) and the git-push arm. Move the final state-event
broadcast (duplicated in both call sites today) into
SyncState::publish.
- staging match block) and the git-push arm. Move the final state-event
broadcast (duplicated in both call sites today) into
- Fold
GraspServer,PushOutcome, and thegrasp_*URL helpers intosync_state.rsand deletepush.rs. announce_repository_and_pushshrinks to: connect -> sign/broadcast announcement ->state.push_to_grasps(plan, ...)-> retraction on failure.- Validate: full
cargo testfor the signed crates; outcomes must be identical (same events, samePushOutcome).
3. Add step-by-step progress
- Add
PushStep,StepStatus,PushProgresstosync_state.rs. Steps update viaprogress.update(cx, |progress, cx| { ...; cx.notify(); })from the foreground orchestration task; per-server failures carry their reason inline (Failed("relay did not confirm the event")). Backend::push_repository/push_checkout/create_repository/publish_local_repoacceptEntity<PushProgress>from the caller.- Keep the
pushing_reposguard andPushOutcomereturns as-is;PushProgressis additive. - Validate: unit test in
sync_state.rsdrivingpush_to_graspswith a mock signer against a fake server list, asserting the emitted step sequence.
4. Surface it in the UI
RepoStoreholdspush_progress: Option<Entity<PushProgress>>for the running push.RepoDetailViewrenders a checklist whilepushing: spinner onRunning, check onDone, cross plus reason onFailed. The existing loading state on header/push buttons stays.- Dialogs: extend
DialogProgresswithsteps: Option<Entity<PushProgress>>so the create/publish dialogs show the same checklist. - Validate: manual run of the push and create-repository flows; the
partial-failure path must still show
render_push_warning_bannerafter the checklist.
Each step lands as a compiling, testable commit: 1 and 2 are mechanical refactors, 3 is additive API, 4 is the visible payoff.
Open decisions
PushKindenum vs. keeping the push closure: the enum matches current call sites; keep the closure if other push modes (e.g. PR namespace pushes) are planned.PushProgressas anEntitypassed in (recommended) vs. aBackendEvent::PushProgressbroadcast thatRepoStoreaccumulates.