app

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

commit 2c0f0667233d9f2fa1865ea8501c4f21d0e1e30d
parent 0aa809291cebebbd33452dfde12ff82dc3d684f9
Author: triesap <tyson@radroots.org>
Date:   Mon, 25 May 2026 09:22:29 +0000

sync: preserve relay-ingested farm visibility

Diffstat:
Mcrates/shared/sqlite/src/local_interop.rs | 275++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 273 insertions(+), 2 deletions(-)

diff --git a/crates/shared/sqlite/src/local_interop.rs b/crates/shared/sqlite/src/local_interop.rs @@ -355,7 +355,7 @@ impl<'a> AppLocalInteropRepository<'a> { display_name: display_name.clone(), readiness: FarmReadiness::Incomplete, }; - self.upsert_farm_summary(&saved_farm)?; + self.upsert_local_work_farm_summary(&saved_farm)?; let owner_account_id = record .owner_account_id .clone() @@ -472,10 +472,16 @@ impl<'a> AppLocalInteropRepository<'a> { }; let display_name = string_at(&content, &["name"]).unwrap_or_else(|| "Local farm".to_owned()); + let readiness = match signed_farm_readiness(&content, tags) { + Some(readiness) => readiness, + None => self + .load_farm_readiness(farm_id)? + .unwrap_or(FarmReadiness::Incomplete), + }; self.upsert_farm_summary(&FarmSummary { farm_id, display_name, - readiness: signed_farm_readiness(&content, tags).unwrap_or(FarmReadiness::Incomplete), + readiness, })?; Ok(Some(ProjectionRecord { kind: "farm", @@ -655,6 +661,32 @@ impl<'a> AppLocalInteropRepository<'a> { Ok(()) } + fn upsert_local_work_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> { + self.connection + .execute( + "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) + VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + ON CONFLICT(id) DO UPDATE SET + display_name = excluded.display_name, + readiness = CASE + WHEN farms.readiness = 'ready' AND excluded.readiness = 'incomplete' + THEN farms.readiness + ELSE excluded.readiness + END, + updated_at = excluded.updated_at", + params![ + farm.farm_id.to_string(), + farm.display_name.as_str(), + farm_readiness_storage_key(farm.readiness), + ], + ) + .map_err(|source| AppSqliteError::Query { + operation: "upsert local interop local work farm summary", + source, + })?; + Ok(()) + } + fn mark_farm_buyer_visible( &self, farm_id: FarmId, @@ -762,6 +794,25 @@ impl<'a> AppLocalInteropRepository<'a> { }) } + fn load_farm_readiness( + &self, + farm_id: FarmId, + ) -> Result<Option<FarmReadiness>, AppSqliteError> { + self.connection + .query_row( + "SELECT readiness FROM farms WHERE id = ?1 LIMIT 1", + [farm_id.to_string()], + |row| row.get::<_, String>(0), + ) + .optional() + .map_err(|source| AppSqliteError::Query { + operation: "load local interop farm readiness", + source, + })? + .map(|readiness| farm_readiness_from_storage_key(readiness.as_str())) + .transpose() + } + fn ensure_signed_listing_availability_window( &self, farm_id: FarmId, @@ -1511,6 +1562,16 @@ fn farm_readiness_storage_key(readiness: FarmReadiness) -> &'static str { } } +fn farm_readiness_from_storage_key(readiness: &str) -> Result<FarmReadiness, AppSqliteError> { + match readiness { + "incomplete" => Ok(FarmReadiness::Incomplete), + "ready" => Ok(FarmReadiness::Ready), + _ => Err(AppSqliteError::InvalidProjection { + reason: "farm readiness storage key is invalid", + }), + } +} + #[cfg(test)] mod tests { use std::collections::BTreeSet; @@ -1566,6 +1627,58 @@ mod tests { } } + fn signed_farm_record( + record_id: &str, + event_id: &str, + source_runtime: SourceRuntime, + owner_pubkey: &str, + farm_key: &str, + readiness: &str, + display_name: &str, + ) -> LocalEventRecordInput { + LocalEventRecordInput { + record_id: record_id.to_owned(), + family: LocalRecordFamily::SignedEvent, + status: LocalRecordStatus::Published, + source_runtime, + created_at_ms: 1100, + inserted_at_ms: 1101, + owner_account_id: Some("seller-account".to_owned()), + owner_pubkey: Some(owner_pubkey.to_owned()), + farm_id: Some(farm_key.to_owned()), + listing_addr: None, + local_work_json: None, + event_id: Some(event_id.to_owned()), + event_kind: Some(KIND_FARM), + event_pubkey: Some(owner_pubkey.to_owned()), + event_created_at: Some(1100), + event_tags_json: Some(json!([ + ["d", farm_key], + ["t", format!("radroots:readiness:{readiness}")] + ])), + event_content: Some( + json!({ + "d_tag": farm_key, + "name": display_name, + "tags": [format!("radroots:readiness:{readiness}")] + }) + .to_string(), + ), + event_sig: Some("signature".to_owned()), + raw_event_json: Some(json!({ + "id": event_id, + "kind": KIND_FARM, + "pubkey": owner_pubkey, + })), + outbox_status: PublishOutboxStatus::Acknowledged, + relay_set_fingerprint: Some("relay-set".to_owned()), + relay_delivery_json: Some(json!({ + "state": "acknowledged", + "acknowledged_relays": ["ws://127.0.0.1:1234/"] + })), + } + } + fn signed_listing_record( record_id: &str, farm_key: &str, @@ -2698,6 +2811,164 @@ mod tests { } #[test] + fn local_work_farm_import_preserves_duplicate_relay_signed_ready_farm() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let relay_events = local_events_store(); + let shared_events = local_events_store(); + let farm_uuid = Uuid::from_u128(0x55555555555545558555555555555555); + let farm_key = app_d_tag_from_uuid(farm_uuid); + let signed_event_id = "event-app-relay-ready-farm"; + let relay_record = relay_events + .append_record(&signed_farm_record( + "app:relay_event:farm-ready", + signed_event_id, + SourceRuntime::App, + "app-seller-pubkey", + farm_key.as_str(), + "ready", + "Relay Ready Farm", + )) + .expect("append relay farm"); + let direct_report = app_store + .import_local_event_records(&[relay_record]) + .expect("direct relay import"); + let local_farm_record = app_local_work_record( + "app:local_work:farm:ready-preserve", + farm_key.as_str(), + json!({ + "record_kind": "farm_config_v1", + "document": { + "selection": { + "account": "seller-account", + "farm_d_tag": farm_key + }, + "profile": { + "display_name": "Draft Farm" + }, + "farm": { + "d_tag": farm_key, + "name": "Draft Farm" + } + } + }), + ); + shared_events + .append_record(&local_farm_record) + .expect("append local farm work"); + shared_events + .append_record(&signed_farm_record( + "app:signed_event:farm-ready", + signed_event_id, + SourceRuntime::App, + "app-seller-pubkey", + farm_key.as_str(), + "ready", + "Relay Ready Farm", + )) + .expect("append duplicate signed farm"); + + let shared_report = app_store + .import_shared_local_events_from_store(&shared_events) + .expect("import shared local work after relay"); + let stored_farm: (String, String, String) = app_store + .connection() + .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { + Ok((row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .expect("load farm"); + + assert_eq!(direct_report.imported_records, 1); + assert_eq!(shared_report.imported_records, 1); + assert_eq!(shared_report.skipped_records, 1); + assert_eq!(stored_farm.0, farm_uuid.to_string()); + assert_eq!(stored_farm.1, "Draft Farm"); + assert_eq!(stored_farm.2, "ready"); + } + + #[test] + fn signed_farm_without_readiness_preserves_listing_visible_farm() { + let app_store = + AppSqliteStore::open(DatabaseTarget::InMemory).expect("open app sqlite store"); + let events = local_events_store(); + let farm_key = "SIGNEDFARMAAAAAAAAAAAA"; + let listing_key = "SIGNEDLISTINGBBBBBBBB"; + let expected_farm_id = deterministic_farm_id(Some("seller-pubkey"), farm_key); + events + .append_record(&signed_market_listing_record( + "visible-listing", + "seller-pubkey", + farm_key, + listing_key, + "Relay Ready Eggs", + "8", + "active", + "pickup", + "West barn pickup", + 4_102_444_800, + 4_102_531_200, + LocalRecordStatus::Published, + PublishOutboxStatus::Acknowledged, + )) + .expect("append visible listing"); + events + .append_record(&LocalEventRecordInput { + record_id: "cli:signed_event:farm:no-readiness".to_owned(), + family: LocalRecordFamily::SignedEvent, + status: LocalRecordStatus::Published, + source_runtime: SourceRuntime::Cli, + created_at_ms: 1200, + inserted_at_ms: 1201, + owner_account_id: Some("seller-account".to_owned()), + owner_pubkey: Some("seller-pubkey".to_owned()), + farm_id: Some(farm_key.to_owned()), + listing_addr: None, + local_work_json: None, + event_id: Some("event-farm-no-readiness".to_owned()), + event_kind: Some(KIND_FARM), + event_pubkey: Some("seller-pubkey".to_owned()), + event_created_at: Some(1200), + event_tags_json: Some(json!([["d", farm_key]])), + event_content: Some( + json!({ + "d_tag": farm_key, + "name": "Relay Ready Farm" + }) + .to_string(), + ), + event_sig: Some("signature".to_owned()), + raw_event_json: Some(json!({ + "id": "event-farm-no-readiness", + "kind": KIND_FARM, + "pubkey": "seller-pubkey" + })), + outbox_status: PublishOutboxStatus::Acknowledged, + relay_set_fingerprint: Some("relay-set".to_owned()), + relay_delivery_json: Some(json!({ + "state": "acknowledged", + "acknowledged_relays": ["ws://127.0.0.1:1234/"] + })), + }) + .expect("append farm without readiness"); + + let report = app_store + .import_shared_local_events_from_store(&events) + .expect("import listing and farm"); + let stored_farm: (String, String, String) = app_store + .connection() + .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { + Ok((row.get(0)?, row.get(1)?, row.get(2)?)) + }) + .expect("load farm"); + + assert_eq!(report.imported_records, 2); + assert_eq!(stored_farm.0, expected_farm_id.to_string()); + assert_eq!(stored_farm.1, "Relay Ready Farm"); + assert_eq!(stored_farm.2, "ready"); + assert_eq!(buyer_listing_titles(&app_store), vec!["Relay Ready Eggs"]); + } + + #[test] fn maps_acknowledged_signed_listing_lifecycle_statuses() { for (status_tag, expected_product_status) in [ ("active", "published"),