app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 4504af9d71441f0ddfcb6859489c1540a9d21971
parent f4b336d78f72501d8bb76010de02640b23783e03
Author: triesap <tyson@radroots.org>
Date:   Sun, 24 May 2026 21:51:37 +0000

app: enqueue order sync after recovery retry

Diffstat:
Mcrates/launchers/desktop/src/runtime.rs | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 132 insertions(+), 11 deletions(-)

diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs @@ -1484,15 +1484,7 @@ impl DesktopAppRuntimeState { )?; } let pending_changed = if matches!(buyer_context, BuyerContext::Account(_)) { - self.enqueue_selected_account_sync_operations(vec![pending_sync_upsert( - SyncAggregateRef::Order(order_id), - order_sync_payload( - order_id, - order_detail.farm_id, - "place_personal_order", - Some("needs_action"), - ), - )])? + self.enqueue_selected_account_order_sync_operation(order_id, order_detail.farm_id)? } else { false }; @@ -1540,6 +1532,12 @@ impl DesktopAppRuntimeState { )? }; if order_changed { + if matches!(buyer_context, BuyerContext::Account(_)) { + let _ = self.enqueue_selected_account_order_sync_operation( + order_export.order_id, + order_export.farm_id, + )?; + } refreshed_order_id.get_or_insert(record.order_id); changed = true; } @@ -3224,6 +3222,53 @@ impl DesktopAppRuntimeState { self.refresh_selected_account_sync() } + fn enqueue_selected_account_order_sync_operation( + &mut self, + order_id: OrderId, + farm_id: FarmId, + ) -> Result<bool, AppSqliteError> { + self.enqueue_selected_account_sync_operation_once(pending_sync_upsert( + SyncAggregateRef::Order(order_id), + order_sync_payload( + order_id, + farm_id, + "place_personal_order", + Some("needs_action"), + ), + )) + } + + fn enqueue_selected_account_sync_operation_once( + &mut self, + operation: PendingSyncOperation, + ) -> Result<bool, AppSqliteError> { + let Some(account_id) = self + .state_store + .identity_projection() + .selected_account + .as_ref() + .map(|account| account.account.account_id.clone()) + else { + return Ok(false); + }; + let already_enqueued = { + let Some(sqlite_store) = self.sqlite_store.as_ref() else { + return Ok(false); + }; + let existing = sqlite_store.load_pending_sync_operations(account_id.as_str())?; + existing.iter().any(|pending| { + pending.operation.aggregate == operation.aggregate + && pending.operation.operation == operation.operation + && pending.operation.payload_json == operation.payload_json + }) + }; + if already_enqueued { + return self.refresh_selected_account_sync(); + } + + self.enqueue_selected_account_sync_operations(vec![operation]) + } + fn selected_account_id(&self) -> Result<String, DesktopAppRuntimeFarmSetupError> { self.selected_account_for_farm_setup() .map(|account| account.account.account_id.clone()) @@ -8658,6 +8703,23 @@ mod tests { .expect("buyer order should place") ); let order_id = runtime.summary().personal_projection.orders.list.rows[0].order_id; + let order_farm_id = runtime + .summary() + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail") + .farm_id; + assert_eq!( + pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), order_id), + vec![super::order_sync_payload( + order_id, + order_farm_id, + "place_personal_order", + Some("needs_action") + )] + ); { let state = runtime.lock_state_mut(); @@ -8790,6 +8852,15 @@ mod tests { .generate_local_account(Some("Buyer".to_owned())) .expect("account should generate") ); + let buyer_account_id = runtime + .summary() + .settings_account_projection + .selected_account + .as_ref() + .expect("selected account") + .account + .account_id + .clone(); assert!( runtime .select_active_surface(ActiveSurface::Personal) @@ -8840,8 +8911,15 @@ mod tests { assert!(summary.personal_projection.cart.cart.lines.is_empty()); assert!(!summary.personal_projection.cart.checkout.can_place_order); assert_eq!(summary.personal_projection.orders.list.rows.len(), 1); - let order_id = { + let (order_id, order_farm_id) = { let visible_order_id = summary.personal_projection.orders.list.rows[0].order_id; + let order_farm_id = summary + .personal_projection + .orders + .detail + .as_ref() + .expect("buyer order detail should remain visible after coordination failure") + .farm_id; assert_eq!( summary .personal_projection @@ -8870,8 +8948,17 @@ mod tests { assert!(coordination.record_id.is_some()); assert!(coordination.payload_json.is_some()); assert!(coordination.last_error_message.is_some()); - order_id + (order_id, order_farm_id) }; + assert!( + pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), order_id).is_empty() + ); + let expected_order_sync_payload = super::order_sync_payload( + order_id, + order_farm_id, + "place_personal_order", + Some("needs_action"), + ); { let state = runtime.lock_state_mut(); let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store"); @@ -8891,6 +8978,10 @@ mod tests { ); let summary_after_retry = runtime.summary(); assert_eq!( + pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), order_id), + vec![expected_order_sync_payload.clone()] + ); + assert_eq!( summary_after_retry .personal_projection .orders @@ -8951,6 +9042,10 @@ mod tests { .retry_pending_personal_order_coordination() .expect("same-session synced buyer order recovery retry should be idempotent") ); + assert_eq!( + pending_order_sync_payloads(&runtime, buyer_account_id.as_str(), order_id), + vec![expected_order_sync_payload] + ); drop(runtime); let restarted_runtime = restart_runtime(paths.clone()); @@ -8987,6 +9082,11 @@ mod tests { .retry_pending_personal_order_coordination() .expect("synced buyer order recovery retry should be idempotent") ); + assert_eq!( + pending_order_sync_payloads(&restarted_runtime, buyer_account_id.as_str(), order_id) + .len(), + 1 + ); cleanup_bootstrapped_runtime_paths(&paths); } @@ -12465,6 +12565,27 @@ mod tests { .expect("shared local records should list") } + fn pending_order_sync_payloads( + runtime: &DesktopAppRuntime, + account_id: &str, + order_id: OrderId, + ) -> Vec<String> { + runtime + .lock_state() + .sqlite_store + .as_ref() + .expect("sqlite store") + .load_pending_sync_operations(account_id) + .expect("pending sync operations should load") + .into_iter() + .filter(|pending| { + pending.operation.operation == SyncOperationKind::Upsert + && matches!(pending.operation.aggregate, SyncAggregateRef::Order(id) if id == order_id) + }) + .map(|pending| pending.operation.payload_json) + .collect() + } + fn block_shared_local_events_database(paths: &AppDesktopRuntimePaths) { let database_path = paths .shared_local_events_database_path()