sdk.rs (88511B)
1 use std::{ 2 fmt, io, 3 path::{Path, PathBuf}, 4 sync::{ 5 Arc, Condvar, Mutex, MutexGuard, 6 atomic::{AtomicBool, Ordering}, 7 mpsc::{self, Receiver, SyncSender, TrySendError}, 8 }, 9 thread::{self, JoinHandle}, 10 time::{Duration, Instant}, 11 }; 12 13 use radroots_authority::{RadrootsActorContext, RadrootsLocalEventSigner}; 14 use radroots_events::{ 15 RadrootsNostrEvent, RadrootsNostrEventPtr, 16 contract::RadrootsActorRole, 17 farm::RadrootsFarm, 18 listing::RadrootsListing, 19 order::{ 20 RadrootsOrderCancellation, RadrootsOrderDecision, RadrootsOrderRequest, 21 RadrootsOrderRevisionDecision, RadrootsOrderRevisionProposal, 22 }, 23 }; 24 use radroots_nostr::prelude::RadrootsNostrKeys; 25 use radroots_sdk::{ 26 FARM_PUBLISH_OPERATION_KIND, FarmEnqueuePublishRequest, FarmEnqueueReceipt, IntegrityReceipt, 27 IntegrityRequest, LISTING_PUBLISH_OPERATION_KIND, ListingEnqueuePublishRequest, 28 ListingEnqueueReceipt, ORDER_CANCELLATION_OPERATION_KIND, ORDER_DECISION_OPERATION_KIND, 29 ORDER_REVISION_DECISION_OPERATION_KIND, ORDER_REVISION_PROPOSAL_OPERATION_KIND, 30 ORDER_SUBMIT_OPERATION_KIND, OrderCancellationEnqueueRequest, OrderCancellationReceipt, 31 OrderDecisionEnqueueRequest, OrderDecisionReceipt, OrderEvidenceIngestRequest, 32 OrderRequestEvidenceIngestRequest, OrderRevisionDecisionEnqueueRequest, 33 OrderRevisionDecisionReceipt, OrderRevisionProposalEnqueueRequest, 34 OrderRevisionProposalReceipt, OrderSubmitEnqueueRequest, OrderSubmitReceipt, RadrootsSdk, 35 RadrootsSdkError, RadrootsSdkStoragePaths, RestoreReceipt, RestoreRequest, 36 SdkBackupVerification, SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, StorageStatusReceipt, 37 StorageStatusRequest, SyncStatusReceipt, SyncStatusRequest, 38 }; 39 use radroots_sdk::{SdkMutationState, SdkRelayTargetPolicy}; 40 use serde::Serialize; 41 use serde_json::{Value, json}; 42 use thiserror::Error; 43 use tokio::runtime::Builder as TokioRuntimeBuilder; 44 45 use crate::AppDesktopRuntimePaths; 46 47 pub const APP_SDK_STORAGE_DIR_NAME: &str = "sdk"; 48 pub const APP_SDK_DEFAULT_COMMAND_QUEUE_CAPACITY: usize = 32; 49 50 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 51 pub enum AppSdkRelayUrlPolicy { 52 Public, 53 Localhost, 54 } 55 56 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 57 pub enum AppSdkLifecycleState { 58 Starting, 59 Ready, 60 Degraded, 61 Pausing, 62 Paused, 63 Restoring, 64 RebuildingProjections, 65 ShuttingDown, 66 Stopped, 67 } 68 69 #[derive(Clone, Debug, Eq, PartialEq)] 70 pub struct AppSdkConfig { 71 pub storage_root: PathBuf, 72 pub relay_urls: Vec<String>, 73 pub relay_url_policy: AppSdkRelayUrlPolicy, 74 pub command_queue_capacity: usize, 75 } 76 77 #[derive(Clone, Debug, Eq, PartialEq)] 78 pub struct AppSdkStoragePaths { 79 pub event_store_path: PathBuf, 80 pub outbox_path: PathBuf, 81 } 82 83 #[derive(Clone, Debug, PartialEq)] 84 pub struct AppSdkRuntimeIssue { 85 pub code: String, 86 pub class: String, 87 pub retryable: bool, 88 pub message: String, 89 pub recovery_actions: Vec<String>, 90 pub detail_json: Value, 91 } 92 93 #[derive(Clone, Debug, PartialEq)] 94 pub struct AppSdkRuntimeStatus { 95 pub state: AppSdkLifecycleState, 96 pub storage_root: PathBuf, 97 pub relay_urls: Vec<String>, 98 pub relay_url_policy: AppSdkRelayUrlPolicy, 99 pub storage_paths: Option<AppSdkStoragePaths>, 100 pub last_issue: Option<AppSdkRuntimeIssue>, 101 pub projection_lifecycle: AppSdkProjectionLifecycleStatus, 102 } 103 104 #[derive(Clone, Debug, PartialEq)] 105 pub struct AppSdkDiagnostics { 106 pub runtime: AppSdkRuntimeStatus, 107 pub storage: AppSdkStorageDiagnostics, 108 pub integrity: AppSdkIntegrityDiagnostics, 109 pub sync: AppSdkSyncDiagnostics, 110 } 111 112 #[derive(Clone, Debug, PartialEq)] 113 pub struct AppSdkStorageDiagnostics { 114 pub storage_kind: String, 115 pub paths: Option<AppSdkStoragePaths>, 116 pub event_store: AppSdkEventStoreDiagnostics, 117 pub outbox: AppSdkOutboxDiagnostics, 118 } 119 120 #[derive(Clone, Debug, PartialEq)] 121 pub struct AppSdkSqliteStoreDiagnostics { 122 pub schema_version: i64, 123 pub journal_mode: String, 124 pub foreign_keys_enabled: bool, 125 pub busy_timeout_ms: i64, 126 pub integrity_ok: bool, 127 pub integrity_result: String, 128 } 129 130 #[derive(Clone, Debug, PartialEq)] 131 pub struct AppSdkEventStoreDiagnostics { 132 pub store: AppSdkSqliteStoreDiagnostics, 133 pub total_events: i64, 134 pub projection_eligible_events: i64, 135 pub relay_observations: i64, 136 pub last_event_seq: Option<i64>, 137 pub last_event_updated_at_ms: Option<i64>, 138 } 139 140 #[derive(Clone, Debug, PartialEq)] 141 pub struct AppSdkOutboxDiagnostics { 142 pub store: AppSdkSqliteStoreDiagnostics, 143 pub total_events: i64, 144 pub pending_events: i64, 145 pub retryable_events: i64, 146 pub terminal_events: i64, 147 pub failed_terminal_events: i64, 148 pub ready_signed_events: i64, 149 pub publishing_events: i64, 150 pub last_attempt_at_ms: Option<i64>, 151 pub last_error: Option<String>, 152 } 153 154 #[derive(Clone, Debug, PartialEq)] 155 pub struct AppSdkIntegrityDiagnostics { 156 pub checked_paths: Vec<PathBuf>, 157 pub event_store_ok: bool, 158 pub outbox_ok: bool, 159 pub event_store_result: String, 160 pub outbox_result: String, 161 } 162 163 #[derive(Clone, Debug, PartialEq)] 164 pub struct AppSdkSyncDiagnostics { 165 pub source: String, 166 pub observed_at_ms: i64, 167 pub event_store: AppSdkSyncEventStoreDiagnostics, 168 pub outbox: AppSdkSyncOutboxDiagnostics, 169 pub relay_targets: AppSdkSyncRelayTargetDiagnostics, 170 } 171 172 #[derive(Clone, Debug, PartialEq)] 173 pub struct AppSdkSyncEventStoreDiagnostics { 174 pub total_events: i64, 175 pub projection_eligible_events: i64, 176 pub relay_observations: i64, 177 pub last_event_seq: Option<i64>, 178 pub last_event_updated_at_ms: Option<i64>, 179 } 180 181 #[derive(Clone, Debug, PartialEq)] 182 pub struct AppSdkSyncOutboxDiagnostics { 183 pub total_events: i64, 184 pub pending_events: i64, 185 pub retryable_events: i64, 186 pub terminal_events: i64, 187 pub failed_terminal_events: i64, 188 pub ready_signed_events: i64, 189 pub publishing_events: i64, 190 pub last_attempt_at_ms: Option<i64>, 191 pub last_error: Option<String>, 192 } 193 194 #[derive(Clone, Debug, PartialEq)] 195 pub struct AppSdkSyncRelayTargetDiagnostics { 196 pub configured_count: usize, 197 pub configured_relays: Vec<String>, 198 } 199 200 #[derive(Clone, Debug, PartialEq)] 201 pub struct AppSdkRestorePreflightRequest { 202 pub source: PathBuf, 203 pub overwrite_existing_sdk_storage: bool, 204 } 205 206 pub struct AppSdkFarmPublishRequest { 207 pub actor_account_id: String, 208 pub actor_pubkey: String, 209 pub signer_keys: RadrootsNostrKeys, 210 pub farm: RadrootsFarm, 211 pub target_relays: Vec<String>, 212 pub relay_url_policy: AppSdkRelayUrlPolicy, 213 pub idempotency_key: Option<String>, 214 } 215 216 pub struct AppSdkListingPublishRequest { 217 pub actor_account_id: String, 218 pub actor_pubkey: String, 219 pub signer_keys: RadrootsNostrKeys, 220 pub listing: RadrootsListing, 221 pub target_relays: Vec<String>, 222 pub relay_url_policy: AppSdkRelayUrlPolicy, 223 pub idempotency_key: Option<String>, 224 } 225 226 pub struct AppSdkOrderSubmitRequest { 227 pub actor_account_id: String, 228 pub actor_pubkey: String, 229 pub signer_keys: RadrootsNostrKeys, 230 pub listing_event: RadrootsNostrEventPtr, 231 pub order: RadrootsOrderRequest, 232 pub target_relays: Vec<String>, 233 pub relay_url_policy: AppSdkRelayUrlPolicy, 234 pub idempotency_key: Option<String>, 235 } 236 237 pub struct AppSdkOrderDecisionRequest { 238 pub actor_account_id: String, 239 pub actor_pubkey: String, 240 pub signer_keys: RadrootsNostrKeys, 241 pub request_event: RadrootsNostrEvent, 242 pub request_event_ptr: RadrootsNostrEventPtr, 243 pub decision: RadrootsOrderDecision, 244 pub target_relays: Vec<String>, 245 pub relay_url_policy: AppSdkRelayUrlPolicy, 246 pub idempotency_key: Option<String>, 247 } 248 249 pub struct AppSdkOrderRevisionProposalRequest { 250 pub actor_account_id: String, 251 pub actor_pubkey: String, 252 pub signer_keys: RadrootsNostrKeys, 253 pub evidence_events: Vec<RadrootsNostrEvent>, 254 pub root_event: RadrootsNostrEventPtr, 255 pub previous_event: RadrootsNostrEventPtr, 256 pub proposal: RadrootsOrderRevisionProposal, 257 pub target_relays: Vec<String>, 258 pub relay_url_policy: AppSdkRelayUrlPolicy, 259 pub idempotency_key: Option<String>, 260 } 261 262 pub struct AppSdkOrderRevisionDecisionRequest { 263 pub actor_account_id: String, 264 pub actor_pubkey: String, 265 pub signer_keys: RadrootsNostrKeys, 266 pub evidence_events: Vec<RadrootsNostrEvent>, 267 pub root_event: RadrootsNostrEventPtr, 268 pub previous_event: RadrootsNostrEventPtr, 269 pub decision: RadrootsOrderRevisionDecision, 270 pub target_relays: Vec<String>, 271 pub relay_url_policy: AppSdkRelayUrlPolicy, 272 pub idempotency_key: Option<String>, 273 } 274 275 pub struct AppSdkOrderCancellationRequest { 276 pub actor_account_id: String, 277 pub actor_pubkey: String, 278 pub signer_keys: RadrootsNostrKeys, 279 pub evidence_events: Vec<RadrootsNostrEvent>, 280 pub root_event: RadrootsNostrEventPtr, 281 pub previous_event: RadrootsNostrEventPtr, 282 pub cancellation: RadrootsOrderCancellation, 283 pub target_relays: Vec<String>, 284 pub relay_url_policy: AppSdkRelayUrlPolicy, 285 pub idempotency_key: Option<String>, 286 } 287 288 #[derive(Clone, Debug, Eq, PartialEq)] 289 pub struct AppSdkWorkflowReceipt { 290 pub operation_kind: String, 291 pub expected_event_id: String, 292 pub signed_event_id: String, 293 pub outbox_operation_id: i64, 294 pub outbox_event_id: i64, 295 pub state: String, 296 pub idempotency_digest_prefix: Option<String>, 297 pub actor_pubkey: String, 298 } 299 300 #[derive(Clone, Debug, PartialEq)] 301 pub struct AppSdkRestorePreflightReceipt { 302 pub source: PathBuf, 303 pub destination: PathBuf, 304 pub state: String, 305 pub destination_paths: Option<AppSdkStoragePaths>, 306 pub restored_paths: Option<AppSdkStoragePaths>, 307 pub event_store_path: PathBuf, 308 pub outbox_path: PathBuf, 309 pub manifest_path: PathBuf, 310 pub verification: AppSdkBackupVerificationDiagnostics, 311 pub source_storage: AppSdkStorageDiagnostics, 312 pub projection_lifecycle: AppSdkProjectionLifecycleStatus, 313 } 314 315 #[derive(Clone, Debug, Eq, PartialEq)] 316 pub struct AppSdkBackupVerificationDiagnostics { 317 pub event_store_ok: bool, 318 pub outbox_ok: bool, 319 pub event_store_events: i64, 320 pub outbox_events: i64, 321 } 322 323 #[derive(Clone, Debug, Eq, PartialEq)] 324 pub struct AppSdkProjectionLifecycleStatus { 325 pub state: AppSdkProjectionLifecycleState, 326 pub reason: Option<String>, 327 pub restore_source: Option<PathBuf>, 328 } 329 330 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 331 pub enum AppSdkProjectionLifecycleState { 332 Current, 333 Stale, 334 Rebuilding, 335 } 336 337 #[derive(Debug, Error)] 338 pub enum AppSdkRuntimeError { 339 #[error("app sdk command queue capacity must be greater than zero")] 340 CommandQueueCapacityZero, 341 #[error("failed to start app sdk worker: {0}")] 342 WorkerSpawn(#[from] io::Error), 343 #[error("app sdk command queue is full")] 344 CommandQueueFull, 345 #[error("app sdk command queue is closed")] 346 CommandQueueClosed, 347 #[error("app sdk command response channel is closed")] 348 CommandResponseClosed, 349 #[error("app sdk command failed: {0}")] 350 CommandFailed(AppSdkRuntimeIssue), 351 #[error("app sdk shutdown acknowledgement failed")] 352 ShutdownAck, 353 #[error("app sdk worker failed to join")] 354 WorkerJoin, 355 } 356 357 #[derive(Debug)] 358 pub struct AppSdkRuntime { 359 command_sender: Mutex<Option<SyncSender<AppSdkWorkerCommand>>>, 360 shared: Arc<AppSdkRuntimeShared>, 361 worker: Mutex<Option<JoinHandle<()>>>, 362 } 363 364 #[derive(Debug)] 365 struct AppSdkRuntimeShared { 366 status: Mutex<AppSdkRuntimeStatus>, 367 status_changed: Condvar, 368 shutdown_requested: AtomicBool, 369 } 370 371 enum AppSdkWorkerCommand { 372 StorageStatus(mpsc::Sender<Result<AppSdkStorageDiagnostics, AppSdkRuntimeIssue>>), 373 IntegrityStatus(mpsc::Sender<Result<AppSdkIntegrityDiagnostics, AppSdkRuntimeIssue>>), 374 SyncStatus(mpsc::Sender<Result<AppSdkSyncDiagnostics, AppSdkRuntimeIssue>>), 375 Diagnostics(mpsc::Sender<Result<AppSdkDiagnostics, AppSdkRuntimeIssue>>), 376 RestorePreflight( 377 AppSdkRestorePreflightRequest, 378 mpsc::Sender<Result<AppSdkRestorePreflightReceipt, AppSdkRuntimeIssue>>, 379 ), 380 EnqueueFarmPublish( 381 AppSdkFarmPublishRequest, 382 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 383 ), 384 EnqueueListingPublish( 385 AppSdkListingPublishRequest, 386 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 387 ), 388 EnqueueOrderSubmit( 389 AppSdkOrderSubmitRequest, 390 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 391 ), 392 EnqueueOrderDecision( 393 AppSdkOrderDecisionRequest, 394 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 395 ), 396 EnqueueOrderRevisionProposal( 397 AppSdkOrderRevisionProposalRequest, 398 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 399 ), 400 EnqueueOrderRevisionDecision( 401 AppSdkOrderRevisionDecisionRequest, 402 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 403 ), 404 EnqueueOrderCancellation( 405 AppSdkOrderCancellationRequest, 406 mpsc::Sender<Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue>>, 407 ), 408 BeginProjectionRebuild( 409 mpsc::Sender<Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeIssue>>, 410 ), 411 CompleteProjectionRebuild( 412 mpsc::Sender<Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeIssue>>, 413 ), 414 } 415 416 impl fmt::Debug for AppSdkWorkerCommand { 417 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 418 match self { 419 Self::StorageStatus(_) => formatter.write_str("StorageStatus"), 420 Self::IntegrityStatus(_) => formatter.write_str("IntegrityStatus"), 421 Self::SyncStatus(_) => formatter.write_str("SyncStatus"), 422 Self::Diagnostics(_) => formatter.write_str("Diagnostics"), 423 Self::RestorePreflight(_, _) => formatter.write_str("RestorePreflight"), 424 Self::EnqueueFarmPublish(_, _) => formatter.write_str("EnqueueFarmPublish"), 425 Self::EnqueueListingPublish(_, _) => formatter.write_str("EnqueueListingPublish"), 426 Self::EnqueueOrderSubmit(_, _) => formatter.write_str("EnqueueOrderSubmit"), 427 Self::EnqueueOrderDecision(_, _) => formatter.write_str("EnqueueOrderDecision"), 428 Self::EnqueueOrderRevisionProposal(_, _) => { 429 formatter.write_str("EnqueueOrderRevisionProposal") 430 } 431 Self::EnqueueOrderRevisionDecision(_, _) => { 432 formatter.write_str("EnqueueOrderRevisionDecision") 433 } 434 Self::EnqueueOrderCancellation(_, _) => formatter.write_str("EnqueueOrderCancellation"), 435 Self::BeginProjectionRebuild(_) => formatter.write_str("BeginProjectionRebuild"), 436 Self::CompleteProjectionRebuild(_) => formatter.write_str("CompleteProjectionRebuild"), 437 } 438 } 439 } 440 441 impl AppSdkConfig { 442 pub fn from_desktop_paths(paths: &AppDesktopRuntimePaths, relay_urls: Vec<String>) -> Self { 443 Self::from_app_data_root(paths.app.data.as_path(), relay_urls) 444 } 445 446 pub fn from_app_data_root(data_root: &Path, relay_urls: Vec<String>) -> Self { 447 Self { 448 storage_root: app_sdk_storage_root_from_data_root(data_root), 449 relay_url_policy: app_sdk_relay_url_policy(relay_urls.as_slice()), 450 relay_urls, 451 command_queue_capacity: APP_SDK_DEFAULT_COMMAND_QUEUE_CAPACITY, 452 } 453 } 454 455 pub fn with_command_queue_capacity(mut self, capacity: usize) -> Self { 456 self.command_queue_capacity = capacity; 457 self 458 } 459 } 460 461 impl AppSdkRestorePreflightRequest { 462 pub fn new(source: impl Into<PathBuf>) -> Self { 463 Self { 464 source: source.into(), 465 overwrite_existing_sdk_storage: false, 466 } 467 } 468 469 pub fn with_overwrite_existing_sdk_storage(mut self, overwrite: bool) -> Self { 470 self.overwrite_existing_sdk_storage = overwrite; 471 self 472 } 473 } 474 475 impl AppSdkProjectionLifecycleStatus { 476 pub fn current() -> Self { 477 Self { 478 state: AppSdkProjectionLifecycleState::Current, 479 reason: None, 480 restore_source: None, 481 } 482 } 483 484 fn stale(reason: impl Into<String>, restore_source: Option<PathBuf>) -> Self { 485 Self { 486 state: AppSdkProjectionLifecycleState::Stale, 487 reason: Some(reason.into()), 488 restore_source, 489 } 490 } 491 492 fn rebuilding(reason: impl Into<String>, restore_source: Option<PathBuf>) -> Self { 493 Self { 494 state: AppSdkProjectionLifecycleState::Rebuilding, 495 reason: Some(reason.into()), 496 restore_source, 497 } 498 } 499 } 500 501 impl AppSdkRuntime { 502 pub fn start(config: AppSdkConfig) -> Result<Self, AppSdkRuntimeError> { 503 if config.command_queue_capacity == 0 { 504 return Err(AppSdkRuntimeError::CommandQueueCapacityZero); 505 } 506 507 let initial_status = 508 AppSdkRuntimeStatus::from_config(&config, AppSdkLifecycleState::Starting, None, None); 509 let shared = Arc::new(AppSdkRuntimeShared { 510 status: Mutex::new(initial_status), 511 status_changed: Condvar::new(), 512 shutdown_requested: AtomicBool::new(false), 513 }); 514 let (command_sender, command_receiver) = mpsc::sync_channel(config.command_queue_capacity); 515 let worker_shared = Arc::clone(&shared); 516 let worker = thread::Builder::new() 517 .name("radroots-app-sdk-runtime".to_owned()) 518 .spawn(move || run_app_sdk_worker(config, worker_shared, command_receiver))?; 519 520 Ok(Self { 521 command_sender: Mutex::new(Some(command_sender)), 522 shared, 523 worker: Mutex::new(Some(worker)), 524 }) 525 } 526 527 pub fn status(&self) -> AppSdkRuntimeStatus { 528 lock_status(&self.shared).clone() 529 } 530 531 pub fn storage_status(&self) -> Result<AppSdkStorageDiagnostics, AppSdkRuntimeError> { 532 self.run_command(AppSdkWorkerCommand::StorageStatus) 533 } 534 535 pub fn integrity_status(&self) -> Result<AppSdkIntegrityDiagnostics, AppSdkRuntimeError> { 536 self.run_command(AppSdkWorkerCommand::IntegrityStatus) 537 } 538 539 pub fn sync_status(&self) -> Result<AppSdkSyncDiagnostics, AppSdkRuntimeError> { 540 self.run_command(AppSdkWorkerCommand::SyncStatus) 541 } 542 543 pub fn diagnostics(&self) -> Result<AppSdkDiagnostics, AppSdkRuntimeError> { 544 self.run_command(AppSdkWorkerCommand::Diagnostics) 545 } 546 547 pub fn restore_preflight( 548 &self, 549 request: AppSdkRestorePreflightRequest, 550 ) -> Result<AppSdkRestorePreflightReceipt, AppSdkRuntimeError> { 551 self.run_command(|response_sender| { 552 AppSdkWorkerCommand::RestorePreflight(request, response_sender) 553 }) 554 } 555 556 pub fn enqueue_farm_publish( 557 &self, 558 request: AppSdkFarmPublishRequest, 559 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 560 self.run_command(|response_sender| { 561 AppSdkWorkerCommand::EnqueueFarmPublish(request, response_sender) 562 }) 563 } 564 565 pub fn enqueue_listing_publish( 566 &self, 567 request: AppSdkListingPublishRequest, 568 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 569 self.run_command(|response_sender| { 570 AppSdkWorkerCommand::EnqueueListingPublish(request, response_sender) 571 }) 572 } 573 574 pub fn enqueue_order_submit( 575 &self, 576 request: AppSdkOrderSubmitRequest, 577 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 578 self.run_command(|response_sender| { 579 AppSdkWorkerCommand::EnqueueOrderSubmit(request, response_sender) 580 }) 581 } 582 583 pub fn enqueue_order_decision( 584 &self, 585 request: AppSdkOrderDecisionRequest, 586 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 587 self.run_command(|response_sender| { 588 AppSdkWorkerCommand::EnqueueOrderDecision(request, response_sender) 589 }) 590 } 591 592 pub fn enqueue_order_revision_proposal( 593 &self, 594 request: AppSdkOrderRevisionProposalRequest, 595 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 596 self.run_command(|response_sender| { 597 AppSdkWorkerCommand::EnqueueOrderRevisionProposal(request, response_sender) 598 }) 599 } 600 601 pub fn enqueue_order_revision_decision( 602 &self, 603 request: AppSdkOrderRevisionDecisionRequest, 604 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 605 self.run_command(|response_sender| { 606 AppSdkWorkerCommand::EnqueueOrderRevisionDecision(request, response_sender) 607 }) 608 } 609 610 pub fn enqueue_order_cancellation( 611 &self, 612 request: AppSdkOrderCancellationRequest, 613 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeError> { 614 self.run_command(|response_sender| { 615 AppSdkWorkerCommand::EnqueueOrderCancellation(request, response_sender) 616 }) 617 } 618 619 pub fn begin_projection_rebuild( 620 &self, 621 ) -> Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeError> { 622 self.run_command(AppSdkWorkerCommand::BeginProjectionRebuild) 623 } 624 625 pub fn complete_projection_rebuild( 626 &self, 627 ) -> Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeError> { 628 self.run_command(AppSdkWorkerCommand::CompleteProjectionRebuild) 629 } 630 631 pub fn wait_for_startup(&self, timeout: Duration) -> AppSdkRuntimeStatus { 632 let deadline = Instant::now() 633 .checked_add(timeout) 634 .unwrap_or_else(Instant::now); 635 let mut status = lock_status(&self.shared); 636 loop { 637 if !matches!(status.state, AppSdkLifecycleState::Starting) { 638 return status.clone(); 639 } 640 let now = Instant::now(); 641 if now >= deadline { 642 return status.clone(); 643 } 644 let remaining = deadline.saturating_duration_since(now); 645 let wait_result = self.shared.status_changed.wait_timeout(status, remaining); 646 let (next_status, timeout_result) = wait_result.unwrap_or_else(|poisoned| { 647 let (guard, timeout_result) = poisoned.into_inner(); 648 (guard, timeout_result) 649 }); 650 status = next_status; 651 if timeout_result.timed_out() { 652 return status.clone(); 653 } 654 } 655 } 656 657 pub fn shutdown(&self) -> Result<(), AppSdkRuntimeError> { 658 if matches!(self.status().state, AppSdkLifecycleState::Stopped) { 659 return self.join_worker(); 660 } 661 662 self.shared.shutdown_requested.store(true, Ordering::SeqCst); 663 transition_status_state(&self.shared, AppSdkLifecycleState::ShuttingDown); 664 let command_sender = self 665 .command_sender 666 .lock() 667 .unwrap_or_else(|poisoned| poisoned.into_inner()) 668 .take(); 669 drop(command_sender); 670 self.join_worker() 671 } 672 673 fn join_worker(&self) -> Result<(), AppSdkRuntimeError> { 674 let mut worker = self 675 .worker 676 .lock() 677 .unwrap_or_else(|poisoned| poisoned.into_inner()); 678 let Some(worker) = worker.take() else { 679 return Ok(()); 680 }; 681 worker.join().map_err(|_| AppSdkRuntimeError::WorkerJoin) 682 } 683 684 fn run_command<T>( 685 &self, 686 command: impl FnOnce(mpsc::Sender<Result<T, AppSdkRuntimeIssue>>) -> AppSdkWorkerCommand, 687 ) -> Result<T, AppSdkRuntimeError> { 688 let (response_sender, response_receiver) = mpsc::channel(); 689 let command_sender = { 690 let command_sender = self 691 .command_sender 692 .lock() 693 .unwrap_or_else(|poisoned| poisoned.into_inner()); 694 if self.shared.shutdown_requested.load(Ordering::SeqCst) { 695 return Err(AppSdkRuntimeError::CommandQueueClosed); 696 } 697 command_sender 698 .as_ref() 699 .cloned() 700 .ok_or(AppSdkRuntimeError::CommandQueueClosed)? 701 }; 702 match command_sender.try_send(command(response_sender)) { 703 Ok(()) => {} 704 Err(TrySendError::Full(_)) => return Err(AppSdkRuntimeError::CommandQueueFull), 705 Err(TrySendError::Disconnected(_)) => { 706 return Err(AppSdkRuntimeError::CommandQueueClosed); 707 } 708 } 709 response_receiver 710 .recv() 711 .map_err(|_| AppSdkRuntimeError::CommandResponseClosed)? 712 .map_err(AppSdkRuntimeError::CommandFailed) 713 } 714 } 715 716 impl Drop for AppSdkRuntime { 717 fn drop(&mut self) { 718 let _ = self.shutdown(); 719 } 720 } 721 722 impl From<AppSdkRelayUrlPolicy> for SdkRuntimeRelayUrlPolicy { 723 fn from(policy: AppSdkRelayUrlPolicy) -> Self { 724 match policy { 725 AppSdkRelayUrlPolicy::Public => Self::Public, 726 AppSdkRelayUrlPolicy::Localhost => Self::Localhost, 727 } 728 } 729 } 730 731 impl From<&RadrootsSdkStoragePaths> for AppSdkStoragePaths { 732 fn from(paths: &RadrootsSdkStoragePaths) -> Self { 733 Self { 734 event_store_path: paths.event_store_path.clone(), 735 outbox_path: paths.outbox_path.clone(), 736 } 737 } 738 } 739 740 impl AppSdkRuntimeIssue { 741 fn from_sdk_error(error: &RadrootsSdkError) -> Self { 742 Self { 743 code: error.code().to_owned(), 744 class: sdk_error_class_label(error), 745 retryable: error.retryable(), 746 message: error.to_string(), 747 recovery_actions: error 748 .recovery_actions() 749 .into_iter() 750 .filter_map(|action| serde_json::to_value(action).ok()) 751 .filter_map(|value| value.as_str().map(str::to_owned)) 752 .collect(), 753 detail_json: error.detail_json(), 754 } 755 } 756 757 fn runtime_error(code: &'static str, message: String) -> Self { 758 Self { 759 code: code.to_owned(), 760 class: "runtime".to_owned(), 761 retryable: true, 762 message: message.clone(), 763 recovery_actions: vec!["retry_startup".to_owned()], 764 detail_json: json!({ 765 "code": code, 766 "class": "runtime", 767 "retryable": true, 768 "message": message, 769 "recovery_actions": ["retry_startup"], 770 "detail": {} 771 }), 772 } 773 } 774 775 fn lifecycle_blocked(state: AppSdkLifecycleState) -> Self { 776 Self { 777 code: "sdk_lifecycle_busy".to_owned(), 778 class: "runtime".to_owned(), 779 retryable: true, 780 message: format!("app sdk runtime is {:?}", state), 781 recovery_actions: vec!["wait_for_sdk_lifecycle".to_owned()], 782 detail_json: json!({ 783 "code": "sdk_lifecycle_busy", 784 "class": "runtime", 785 "retryable": true, 786 "state": format!("{state:?}"), 787 "recovery_actions": ["wait_for_sdk_lifecycle"] 788 }), 789 } 790 } 791 } 792 793 impl fmt::Display for AppSdkRuntimeIssue { 794 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 795 write!(formatter, "{}: {}", self.code, self.message) 796 } 797 } 798 799 impl AppSdkRuntimeStatus { 800 fn from_config( 801 config: &AppSdkConfig, 802 state: AppSdkLifecycleState, 803 storage_paths: Option<AppSdkStoragePaths>, 804 last_issue: Option<AppSdkRuntimeIssue>, 805 ) -> Self { 806 Self { 807 state, 808 storage_root: config.storage_root.clone(), 809 relay_urls: config.relay_urls.clone(), 810 relay_url_policy: config.relay_url_policy, 811 storage_paths, 812 last_issue, 813 projection_lifecycle: AppSdkProjectionLifecycleStatus::current(), 814 } 815 } 816 } 817 818 impl From<StorageStatusReceipt> for AppSdkStorageDiagnostics { 819 fn from(receipt: StorageStatusReceipt) -> Self { 820 Self { 821 storage_kind: serialized_label(&receipt.storage), 822 paths: receipt.paths.as_ref().map(AppSdkStoragePaths::from), 823 event_store: AppSdkEventStoreDiagnostics { 824 store: receipt.event_store.store.into(), 825 total_events: receipt.event_store.total_events, 826 projection_eligible_events: receipt.event_store.projection_eligible_events, 827 relay_observations: receipt.event_store.relay_observations, 828 last_event_seq: receipt.event_store.last_event_seq, 829 last_event_updated_at_ms: receipt.event_store.last_event_updated_at_ms, 830 }, 831 outbox: AppSdkOutboxDiagnostics { 832 store: receipt.outbox.store.into(), 833 total_events: receipt.outbox.total_events, 834 pending_events: receipt.outbox.pending_events, 835 retryable_events: receipt.outbox.retryable_events, 836 terminal_events: receipt.outbox.terminal_events, 837 failed_terminal_events: receipt.outbox.failed_terminal_events, 838 ready_signed_events: receipt.outbox.ready_signed_events, 839 publishing_events: receipt.outbox.publishing_events, 840 last_attempt_at_ms: receipt.outbox.last_attempt_at_ms, 841 last_error: receipt.outbox.last_error, 842 }, 843 } 844 } 845 } 846 847 impl From<radroots_sdk::SdkSqliteStoreStatus> for AppSdkSqliteStoreDiagnostics { 848 fn from(status: radroots_sdk::SdkSqliteStoreStatus) -> Self { 849 Self { 850 schema_version: status.schema_version, 851 journal_mode: status.journal_mode, 852 foreign_keys_enabled: status.foreign_keys_enabled, 853 busy_timeout_ms: status.busy_timeout_ms, 854 integrity_ok: status.integrity_ok, 855 integrity_result: status.integrity_result, 856 } 857 } 858 } 859 860 impl From<IntegrityReceipt> for AppSdkIntegrityDiagnostics { 861 fn from(receipt: IntegrityReceipt) -> Self { 862 Self { 863 checked_paths: receipt.checked_paths, 864 event_store_ok: receipt.event_store_ok, 865 outbox_ok: receipt.outbox_ok, 866 event_store_result: receipt.event_store_result, 867 outbox_result: receipt.outbox_result, 868 } 869 } 870 } 871 872 impl From<SyncStatusReceipt> for AppSdkSyncDiagnostics { 873 fn from(receipt: SyncStatusReceipt) -> Self { 874 Self { 875 source: serialized_label(&receipt.source), 876 observed_at_ms: receipt.observed_at_ms, 877 event_store: AppSdkSyncEventStoreDiagnostics { 878 total_events: receipt.event_store.total_events, 879 projection_eligible_events: receipt.event_store.projection_eligible_events, 880 relay_observations: receipt.event_store.relay_observations, 881 last_event_seq: receipt.event_store.last_event_seq, 882 last_event_updated_at_ms: receipt.event_store.last_event_updated_at_ms, 883 }, 884 outbox: AppSdkSyncOutboxDiagnostics { 885 total_events: receipt.outbox.total_events, 886 pending_events: receipt.outbox.pending_events, 887 retryable_events: receipt.outbox.retryable_events, 888 terminal_events: receipt.outbox.terminal_events, 889 failed_terminal_events: receipt.outbox.failed_terminal_events, 890 ready_signed_events: receipt.outbox.ready_signed_events, 891 publishing_events: receipt.outbox.publishing_events, 892 last_attempt_at_ms: receipt.outbox.last_attempt_at_ms, 893 last_error: receipt.outbox.last_error, 894 }, 895 relay_targets: AppSdkSyncRelayTargetDiagnostics { 896 configured_count: receipt.relay_targets.configured_count, 897 configured_relays: receipt.relay_targets.configured_relays, 898 }, 899 } 900 } 901 } 902 903 impl From<SdkBackupVerification> for AppSdkBackupVerificationDiagnostics { 904 fn from(verification: SdkBackupVerification) -> Self { 905 Self { 906 event_store_ok: verification.event_store_ok, 907 outbox_ok: verification.outbox_ok, 908 event_store_events: verification.event_store_events, 909 outbox_events: verification.outbox_events, 910 } 911 } 912 } 913 914 impl AppSdkRestorePreflightReceipt { 915 fn from_restore_receipt( 916 receipt: RestoreReceipt, 917 destination: PathBuf, 918 projection_lifecycle: AppSdkProjectionLifecycleStatus, 919 ) -> Self { 920 Self { 921 source: receipt.source, 922 destination: receipt.destination.unwrap_or(destination), 923 state: serialized_label(&receipt.state), 924 destination_paths: receipt 925 .destination_paths 926 .as_ref() 927 .map(AppSdkStoragePaths::from), 928 restored_paths: receipt 929 .restored_paths 930 .as_ref() 931 .map(AppSdkStoragePaths::from), 932 event_store_path: receipt.event_store_path, 933 outbox_path: receipt.outbox_path, 934 manifest_path: receipt.manifest_path, 935 verification: receipt.verification.into(), 936 source_storage: receipt.manifest.source_status.into(), 937 projection_lifecycle, 938 } 939 } 940 } 941 942 pub fn app_sdk_storage_root_from_data_root(data_root: &Path) -> PathBuf { 943 data_root.join(APP_SDK_STORAGE_DIR_NAME) 944 } 945 946 fn app_sdk_relay_url_policy(relay_urls: &[String]) -> AppSdkRelayUrlPolicy { 947 if relay_urls 948 .iter() 949 .any(|relay_url| relay_url.trim().to_ascii_lowercase().starts_with("ws://")) 950 { 951 AppSdkRelayUrlPolicy::Localhost 952 } else { 953 AppSdkRelayUrlPolicy::Public 954 } 955 } 956 957 fn run_app_sdk_worker( 958 config: AppSdkConfig, 959 shared: Arc<AppSdkRuntimeShared>, 960 command_receiver: Receiver<AppSdkWorkerCommand>, 961 ) { 962 let runtime = match TokioRuntimeBuilder::new_current_thread() 963 .enable_all() 964 .build() 965 { 966 Ok(runtime) => runtime, 967 Err(error) => { 968 replace_status( 969 &shared, 970 AppSdkRuntimeStatus::from_config( 971 &config, 972 AppSdkLifecycleState::Degraded, 973 None, 974 Some(AppSdkRuntimeIssue::runtime_error( 975 "tokio_runtime_init", 976 error.to_string(), 977 )), 978 ), 979 ); 980 run_degraded_worker(config, shared, command_receiver); 981 return; 982 } 983 }; 984 985 let mut sdk = match runtime.block_on(build_sdk_runtime(&config)) { 986 Ok(sdk) => { 987 replace_status( 988 &shared, 989 AppSdkRuntimeStatus::from_config( 990 &config, 991 AppSdkLifecycleState::Ready, 992 sdk.storage_paths().map(AppSdkStoragePaths::from), 993 None, 994 ), 995 ); 996 Some(sdk) 997 } 998 Err(error) => { 999 replace_status( 1000 &shared, 1001 AppSdkRuntimeStatus::from_config( 1002 &config, 1003 AppSdkLifecycleState::Degraded, 1004 None, 1005 Some(AppSdkRuntimeIssue::from_sdk_error(&error)), 1006 ), 1007 ); 1008 None 1009 } 1010 }; 1011 1012 while let Ok(command) = command_receiver.recv() { 1013 if shared.shutdown_requested.load(Ordering::SeqCst) { 1014 break; 1015 } 1016 1017 match command { 1018 AppSdkWorkerCommand::StorageStatus(response_sender) => { 1019 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1020 Err(issue) 1021 } else { 1022 match sdk.as_ref() { 1023 Some(sdk) => runtime 1024 .block_on(sdk.storage_status(StorageStatusRequest::new())) 1025 .map(AppSdkStorageDiagnostics::from) 1026 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)), 1027 None => Err(runtime_unavailable_issue(&shared)), 1028 } 1029 }; 1030 send_worker_result(&shared, response_sender, result); 1031 } 1032 AppSdkWorkerCommand::IntegrityStatus(response_sender) => { 1033 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1034 Err(issue) 1035 } else { 1036 match sdk.as_ref() { 1037 Some(sdk) => runtime 1038 .block_on(sdk.integrity(IntegrityRequest::new())) 1039 .map(AppSdkIntegrityDiagnostics::from) 1040 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)), 1041 None => Err(runtime_unavailable_issue(&shared)), 1042 } 1043 }; 1044 send_worker_result(&shared, response_sender, result); 1045 } 1046 AppSdkWorkerCommand::SyncStatus(response_sender) => { 1047 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1048 Err(issue) 1049 } else { 1050 match sdk.as_ref() { 1051 Some(sdk) => runtime 1052 .block_on(sdk.sync().status(SyncStatusRequest::new())) 1053 .map(AppSdkSyncDiagnostics::from) 1054 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)), 1055 None => Err(runtime_unavailable_issue(&shared)), 1056 } 1057 }; 1058 send_worker_result(&shared, response_sender, result); 1059 } 1060 AppSdkWorkerCommand::Diagnostics(response_sender) => { 1061 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1062 Err(issue) 1063 } else { 1064 match sdk.as_ref() { 1065 Some(sdk) => { 1066 let mut runtime_status = lock_status(&shared).clone(); 1067 runtime_status.last_issue = None; 1068 runtime 1069 .block_on(collect_sdk_diagnostics(sdk, runtime_status)) 1070 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)) 1071 } 1072 None => Err(runtime_unavailable_issue(&shared)), 1073 } 1074 }; 1075 send_worker_result(&shared, response_sender, result); 1076 } 1077 AppSdkWorkerCommand::RestorePreflight(request, response_sender) => { 1078 let result = match sdk.as_ref() { 1079 Some(_) => run_restore_preflight(&runtime, &shared, &config, request), 1080 None => Err(runtime_unavailable_issue(&shared)), 1081 }; 1082 send_worker_result(&shared, response_sender, result); 1083 } 1084 AppSdkWorkerCommand::EnqueueFarmPublish(request, response_sender) => { 1085 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1086 Err(issue) 1087 } else { 1088 match sdk.as_ref() { 1089 Some(sdk) => enqueue_farm_publish_with_sdk(&runtime, sdk, request), 1090 None => Err(runtime_unavailable_issue(&shared)), 1091 } 1092 }; 1093 send_worker_result(&shared, response_sender, result); 1094 } 1095 AppSdkWorkerCommand::EnqueueListingPublish(request, response_sender) => { 1096 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1097 Err(issue) 1098 } else { 1099 match sdk.as_ref() { 1100 Some(sdk) => enqueue_listing_publish_with_sdk(&runtime, sdk, request), 1101 None => Err(runtime_unavailable_issue(&shared)), 1102 } 1103 }; 1104 send_worker_result(&shared, response_sender, result); 1105 } 1106 AppSdkWorkerCommand::EnqueueOrderSubmit(request, response_sender) => { 1107 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1108 Err(issue) 1109 } else { 1110 match sdk.as_ref() { 1111 Some(sdk) => enqueue_order_submit_with_sdk(&runtime, sdk, request), 1112 None => Err(runtime_unavailable_issue(&shared)), 1113 } 1114 }; 1115 send_worker_result(&shared, response_sender, result); 1116 } 1117 AppSdkWorkerCommand::EnqueueOrderDecision(request, response_sender) => { 1118 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1119 Err(issue) 1120 } else { 1121 match sdk.as_ref() { 1122 Some(sdk) => enqueue_order_decision_with_sdk(&runtime, sdk, request), 1123 None => Err(runtime_unavailable_issue(&shared)), 1124 } 1125 }; 1126 send_worker_result(&shared, response_sender, result); 1127 } 1128 AppSdkWorkerCommand::EnqueueOrderRevisionProposal(request, response_sender) => { 1129 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1130 Err(issue) 1131 } else { 1132 match sdk.as_ref() { 1133 Some(sdk) => { 1134 enqueue_order_revision_proposal_with_sdk(&runtime, sdk, request) 1135 } 1136 None => Err(runtime_unavailable_issue(&shared)), 1137 } 1138 }; 1139 send_worker_result(&shared, response_sender, result); 1140 } 1141 AppSdkWorkerCommand::EnqueueOrderRevisionDecision(request, response_sender) => { 1142 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1143 Err(issue) 1144 } else { 1145 match sdk.as_ref() { 1146 Some(sdk) => { 1147 enqueue_order_revision_decision_with_sdk(&runtime, sdk, request) 1148 } 1149 None => Err(runtime_unavailable_issue(&shared)), 1150 } 1151 }; 1152 send_worker_result(&shared, response_sender, result); 1153 } 1154 AppSdkWorkerCommand::EnqueueOrderCancellation(request, response_sender) => { 1155 let result = if let Some(issue) = lifecycle_busy_issue(&shared) { 1156 Err(issue) 1157 } else { 1158 match sdk.as_ref() { 1159 Some(sdk) => enqueue_order_cancellation_with_sdk(&runtime, sdk, request), 1160 None => Err(runtime_unavailable_issue(&shared)), 1161 } 1162 }; 1163 send_worker_result(&shared, response_sender, result); 1164 } 1165 AppSdkWorkerCommand::BeginProjectionRebuild(response_sender) => { 1166 let result = match sdk.as_ref() { 1167 Some(_) => Ok(begin_projection_rebuild(&shared)), 1168 None => Err(runtime_unavailable_issue(&shared)), 1169 }; 1170 send_worker_result(&shared, response_sender, result); 1171 } 1172 AppSdkWorkerCommand::CompleteProjectionRebuild(response_sender) => { 1173 let result = match sdk.as_ref() { 1174 Some(_) => complete_projection_rebuild(&shared), 1175 None => Err(runtime_unavailable_issue(&shared)), 1176 }; 1177 send_worker_result(&shared, response_sender, result); 1178 } 1179 } 1180 } 1181 1182 drop(sdk.take()); 1183 transition_status_state(&shared, AppSdkLifecycleState::Stopped); 1184 } 1185 1186 fn run_degraded_worker( 1187 config: AppSdkConfig, 1188 shared: Arc<AppSdkRuntimeShared>, 1189 command_receiver: Receiver<AppSdkWorkerCommand>, 1190 ) { 1191 while let Ok(command) = command_receiver.recv() { 1192 if shared.shutdown_requested.load(Ordering::SeqCst) { 1193 break; 1194 } 1195 1196 match command { 1197 AppSdkWorkerCommand::StorageStatus(response_sender) => { 1198 send_worker_result( 1199 &shared, 1200 response_sender, 1201 Err(runtime_unavailable_issue(&shared)), 1202 ); 1203 } 1204 AppSdkWorkerCommand::IntegrityStatus(response_sender) => { 1205 send_worker_result( 1206 &shared, 1207 response_sender, 1208 Err(runtime_unavailable_issue(&shared)), 1209 ); 1210 } 1211 AppSdkWorkerCommand::SyncStatus(response_sender) => { 1212 send_worker_result( 1213 &shared, 1214 response_sender, 1215 Err(runtime_unavailable_issue(&shared)), 1216 ); 1217 } 1218 AppSdkWorkerCommand::Diagnostics(response_sender) => { 1219 send_worker_result( 1220 &shared, 1221 response_sender, 1222 Err(runtime_unavailable_issue(&shared)), 1223 ); 1224 } 1225 AppSdkWorkerCommand::RestorePreflight(_, response_sender) => { 1226 send_worker_result( 1227 &shared, 1228 response_sender, 1229 Err(runtime_unavailable_issue(&shared)), 1230 ); 1231 } 1232 AppSdkWorkerCommand::EnqueueFarmPublish(_, response_sender) => { 1233 send_worker_result( 1234 &shared, 1235 response_sender, 1236 Err(runtime_unavailable_issue(&shared)), 1237 ); 1238 } 1239 AppSdkWorkerCommand::EnqueueListingPublish(_, response_sender) => { 1240 send_worker_result( 1241 &shared, 1242 response_sender, 1243 Err(runtime_unavailable_issue(&shared)), 1244 ); 1245 } 1246 AppSdkWorkerCommand::EnqueueOrderSubmit(_, response_sender) => { 1247 send_worker_result( 1248 &shared, 1249 response_sender, 1250 Err(runtime_unavailable_issue(&shared)), 1251 ); 1252 } 1253 AppSdkWorkerCommand::EnqueueOrderDecision(_, response_sender) => { 1254 send_worker_result( 1255 &shared, 1256 response_sender, 1257 Err(runtime_unavailable_issue(&shared)), 1258 ); 1259 } 1260 AppSdkWorkerCommand::EnqueueOrderRevisionProposal(_, response_sender) => { 1261 send_worker_result( 1262 &shared, 1263 response_sender, 1264 Err(runtime_unavailable_issue(&shared)), 1265 ); 1266 } 1267 AppSdkWorkerCommand::EnqueueOrderRevisionDecision(_, response_sender) => { 1268 send_worker_result( 1269 &shared, 1270 response_sender, 1271 Err(runtime_unavailable_issue(&shared)), 1272 ); 1273 } 1274 AppSdkWorkerCommand::EnqueueOrderCancellation(_, response_sender) => { 1275 send_worker_result( 1276 &shared, 1277 response_sender, 1278 Err(runtime_unavailable_issue(&shared)), 1279 ); 1280 } 1281 AppSdkWorkerCommand::BeginProjectionRebuild(response_sender) => { 1282 send_worker_result( 1283 &shared, 1284 response_sender, 1285 Err(runtime_unavailable_issue(&shared)), 1286 ); 1287 } 1288 AppSdkWorkerCommand::CompleteProjectionRebuild(response_sender) => { 1289 send_worker_result( 1290 &shared, 1291 response_sender, 1292 Err(runtime_unavailable_issue(&shared)), 1293 ); 1294 } 1295 } 1296 } 1297 1298 let last_issue = lock_status(&shared).last_issue.clone(); 1299 replace_status( 1300 &shared, 1301 AppSdkRuntimeStatus::from_config(&config, AppSdkLifecycleState::Stopped, None, last_issue), 1302 ); 1303 } 1304 1305 async fn build_sdk_runtime(config: &AppSdkConfig) -> Result<RadrootsSdk, RadrootsSdkError> { 1306 let mut builder = RadrootsSdk::builder() 1307 .directory_storage(config.storage_root.clone()) 1308 .relay_url_policy(config.relay_url_policy.into()); 1309 for relay_url in &config.relay_urls { 1310 builder = builder.relay_url(relay_url.clone()); 1311 } 1312 builder.build().await 1313 } 1314 1315 fn run_restore_preflight( 1316 runtime: &tokio::runtime::Runtime, 1317 shared: &AppSdkRuntimeShared, 1318 config: &AppSdkConfig, 1319 request: AppSdkRestorePreflightRequest, 1320 ) -> Result<AppSdkRestorePreflightReceipt, AppSdkRuntimeIssue> { 1321 if let Some(issue) = lifecycle_busy_issue(shared) { 1322 return Err(issue); 1323 } 1324 transition_status_state(shared, AppSdkLifecycleState::Pausing); 1325 transition_status_state(shared, AppSdkLifecycleState::Paused); 1326 transition_status_state(shared, AppSdkLifecycleState::Restoring); 1327 1328 let restore_request = RestoreRequest::new(request.source.clone()) 1329 .with_destination(config.storage_root.clone()) 1330 .with_overwrite(request.overwrite_existing_sdk_storage) 1331 .dry_run(); 1332 let result = runtime 1333 .block_on(RadrootsSdk::restore(restore_request)) 1334 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)) 1335 .map(|receipt| { 1336 let projection_lifecycle = mark_projections_stale( 1337 shared, 1338 "sdk_restore_preflight", 1339 Some(request.source.clone()), 1340 ); 1341 AppSdkRestorePreflightReceipt::from_restore_receipt( 1342 receipt, 1343 config.storage_root.clone(), 1344 projection_lifecycle, 1345 ) 1346 }); 1347 if result.is_err() { 1348 transition_status_state(shared, AppSdkLifecycleState::Ready); 1349 } 1350 result 1351 } 1352 1353 async fn collect_sdk_diagnostics( 1354 sdk: &RadrootsSdk, 1355 runtime: AppSdkRuntimeStatus, 1356 ) -> Result<AppSdkDiagnostics, RadrootsSdkError> { 1357 let storage = sdk.storage_status(StorageStatusRequest::new()).await?; 1358 let integrity = sdk.integrity(IntegrityRequest::new()).await?; 1359 let sync = sdk.sync().status(SyncStatusRequest::new()).await?; 1360 Ok(AppSdkDiagnostics { 1361 runtime, 1362 storage: storage.into(), 1363 integrity: integrity.into(), 1364 sync: sync.into(), 1365 }) 1366 } 1367 1368 fn enqueue_farm_publish_with_sdk( 1369 runtime: &tokio::runtime::Runtime, 1370 sdk: &RadrootsSdk, 1371 request: AppSdkFarmPublishRequest, 1372 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1373 let actor = sdk_actor_context( 1374 request.actor_pubkey.as_str(), 1375 request.actor_account_id.as_str(), 1376 RadrootsActorRole::Farmer, 1377 )?; 1378 let signer = sdk_local_signer(request.signer_keys)?; 1379 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1380 let mut enqueue = FarmEnqueuePublishRequest::new(actor, request.farm, target_relays); 1381 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1382 enqueue = enqueue 1383 .try_with_idempotency_key(idempotency_key) 1384 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1385 } 1386 let receipt = runtime 1387 .block_on( 1388 sdk.farms() 1389 .enqueue_publish_with_explicit_signer(enqueue, &signer), 1390 ) 1391 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1392 Ok(app_sdk_farm_receipt(receipt, request.actor_pubkey)) 1393 } 1394 1395 fn enqueue_listing_publish_with_sdk( 1396 runtime: &tokio::runtime::Runtime, 1397 sdk: &RadrootsSdk, 1398 request: AppSdkListingPublishRequest, 1399 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1400 let actor = sdk_actor_context( 1401 request.actor_pubkey.as_str(), 1402 request.actor_account_id.as_str(), 1403 RadrootsActorRole::Seller, 1404 )?; 1405 let signer = sdk_local_signer(request.signer_keys)?; 1406 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1407 let mut enqueue = ListingEnqueuePublishRequest::new(actor, request.listing, target_relays); 1408 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1409 enqueue = enqueue 1410 .try_with_idempotency_key(idempotency_key) 1411 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1412 } 1413 let receipt = runtime 1414 .block_on( 1415 sdk.listings() 1416 .enqueue_publish_with_explicit_signer(enqueue, &signer), 1417 ) 1418 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1419 Ok(app_sdk_listing_receipt(receipt, request.actor_pubkey)) 1420 } 1421 1422 fn enqueue_order_submit_with_sdk( 1423 runtime: &tokio::runtime::Runtime, 1424 sdk: &RadrootsSdk, 1425 request: AppSdkOrderSubmitRequest, 1426 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1427 let actor = sdk_actor_context( 1428 request.actor_pubkey.as_str(), 1429 request.actor_account_id.as_str(), 1430 RadrootsActorRole::Buyer, 1431 )?; 1432 let signer = sdk_local_signer(request.signer_keys)?; 1433 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1434 let mut enqueue = 1435 OrderSubmitEnqueueRequest::new(actor, request.listing_event, request.order, target_relays); 1436 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1437 enqueue = enqueue 1438 .try_with_idempotency_key(idempotency_key) 1439 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1440 } 1441 let receipt = runtime 1442 .block_on( 1443 sdk.orders() 1444 .enqueue_submit_with_explicit_signer(enqueue, &signer), 1445 ) 1446 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1447 Ok(app_sdk_order_submit_ack(receipt, request.actor_pubkey)) 1448 } 1449 1450 fn enqueue_order_decision_with_sdk( 1451 runtime: &tokio::runtime::Runtime, 1452 sdk: &RadrootsSdk, 1453 request: AppSdkOrderDecisionRequest, 1454 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1455 let actor = sdk_actor_context( 1456 request.actor_pubkey.as_str(), 1457 request.actor_account_id.as_str(), 1458 RadrootsActorRole::Seller, 1459 )?; 1460 let signer = sdk_local_signer(request.signer_keys)?; 1461 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1462 runtime 1463 .block_on( 1464 sdk.orders() 1465 .ingest_request_evidence(OrderRequestEvidenceIngestRequest::new( 1466 request.request_event, 1467 )), 1468 ) 1469 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1470 let mut enqueue = OrderDecisionEnqueueRequest::new( 1471 actor, 1472 request.request_event_ptr, 1473 request.decision, 1474 target_relays, 1475 ); 1476 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1477 enqueue = enqueue 1478 .try_with_idempotency_key(idempotency_key) 1479 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1480 } 1481 let receipt = runtime 1482 .block_on( 1483 sdk.orders() 1484 .enqueue_decision_with_explicit_signer(enqueue, &signer), 1485 ) 1486 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1487 Ok(app_sdk_order_decision_receipt( 1488 receipt, 1489 request.actor_pubkey, 1490 )) 1491 } 1492 1493 fn enqueue_order_revision_proposal_with_sdk( 1494 runtime: &tokio::runtime::Runtime, 1495 sdk: &RadrootsSdk, 1496 request: AppSdkOrderRevisionProposalRequest, 1497 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1498 let actor = sdk_actor_context( 1499 request.actor_pubkey.as_str(), 1500 request.actor_account_id.as_str(), 1501 RadrootsActorRole::Seller, 1502 )?; 1503 let signer = sdk_local_signer(request.signer_keys)?; 1504 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1505 ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; 1506 let mut enqueue = OrderRevisionProposalEnqueueRequest::new( 1507 actor, 1508 request.root_event, 1509 request.previous_event, 1510 request.proposal, 1511 target_relays, 1512 ); 1513 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1514 enqueue = enqueue 1515 .try_with_idempotency_key(idempotency_key) 1516 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1517 } 1518 let receipt = runtime 1519 .block_on( 1520 sdk.orders() 1521 .enqueue_revision_proposal_with_explicit_signer(enqueue, &signer), 1522 ) 1523 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1524 Ok(app_sdk_order_revision_proposal_receipt( 1525 receipt, 1526 request.actor_pubkey, 1527 )) 1528 } 1529 1530 fn enqueue_order_revision_decision_with_sdk( 1531 runtime: &tokio::runtime::Runtime, 1532 sdk: &RadrootsSdk, 1533 request: AppSdkOrderRevisionDecisionRequest, 1534 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1535 let actor = sdk_actor_context( 1536 request.actor_pubkey.as_str(), 1537 request.actor_account_id.as_str(), 1538 RadrootsActorRole::Buyer, 1539 )?; 1540 let signer = sdk_local_signer(request.signer_keys)?; 1541 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1542 ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; 1543 let mut enqueue = OrderRevisionDecisionEnqueueRequest::new( 1544 actor, 1545 request.root_event, 1546 request.previous_event, 1547 request.decision, 1548 target_relays, 1549 ); 1550 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1551 enqueue = enqueue 1552 .try_with_idempotency_key(idempotency_key) 1553 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1554 } 1555 let receipt = runtime 1556 .block_on( 1557 sdk.orders() 1558 .enqueue_revision_decision_with_explicit_signer(enqueue, &signer), 1559 ) 1560 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1561 Ok(app_sdk_order_revision_decision_receipt( 1562 receipt, 1563 request.actor_pubkey, 1564 )) 1565 } 1566 1567 fn enqueue_order_cancellation_with_sdk( 1568 runtime: &tokio::runtime::Runtime, 1569 sdk: &RadrootsSdk, 1570 request: AppSdkOrderCancellationRequest, 1571 ) -> Result<AppSdkWorkflowReceipt, AppSdkRuntimeIssue> { 1572 let actor = sdk_actor_context( 1573 request.actor_pubkey.as_str(), 1574 request.actor_account_id.as_str(), 1575 RadrootsActorRole::Buyer, 1576 )?; 1577 let signer = sdk_local_signer(request.signer_keys)?; 1578 let target_relays = sdk_relay_targets(request.target_relays, request.relay_url_policy)?; 1579 ingest_order_evidence_with_sdk(runtime, sdk, request.evidence_events)?; 1580 let mut enqueue = OrderCancellationEnqueueRequest::new( 1581 actor, 1582 request.root_event, 1583 request.previous_event, 1584 request.cancellation, 1585 target_relays, 1586 ); 1587 if let Some(idempotency_key) = request.idempotency_key.as_deref() { 1588 enqueue = enqueue 1589 .try_with_idempotency_key(idempotency_key) 1590 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1591 } 1592 let receipt = runtime 1593 .block_on( 1594 sdk.orders() 1595 .enqueue_cancellation_with_explicit_signer(enqueue, &signer), 1596 ) 1597 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1598 Ok(app_sdk_order_cancellation_receipt( 1599 receipt, 1600 request.actor_pubkey, 1601 )) 1602 } 1603 1604 fn ingest_order_evidence_with_sdk( 1605 runtime: &tokio::runtime::Runtime, 1606 sdk: &RadrootsSdk, 1607 evidence_events: Vec<RadrootsNostrEvent>, 1608 ) -> Result<(), AppSdkRuntimeIssue> { 1609 for event in evidence_events { 1610 runtime 1611 .block_on( 1612 sdk.orders() 1613 .ingest_evidence(OrderEvidenceIngestRequest::new(event)), 1614 ) 1615 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error))?; 1616 } 1617 Ok(()) 1618 } 1619 1620 fn sdk_actor_context( 1621 actor_pubkey: &str, 1622 actor_account_id: &str, 1623 role: RadrootsActorRole, 1624 ) -> Result<RadrootsActorContext, AppSdkRuntimeIssue> { 1625 RadrootsActorContext::local_account(actor_pubkey, actor_account_id.to_owned(), [role]).map_err( 1626 |error| AppSdkRuntimeIssue::runtime_error("sdk_actor_context_invalid", error.to_string()), 1627 ) 1628 } 1629 1630 fn sdk_local_signer( 1631 keys: RadrootsNostrKeys, 1632 ) -> Result<RadrootsLocalEventSigner, AppSdkRuntimeIssue> { 1633 RadrootsLocalEventSigner::new(keys).map_err(|error| { 1634 AppSdkRuntimeIssue::runtime_error("sdk_signer_init_failed", error.to_string()) 1635 }) 1636 } 1637 1638 fn sdk_relay_targets( 1639 relays: Vec<String>, 1640 policy: AppSdkRelayUrlPolicy, 1641 ) -> Result<SdkRelayTargetPolicy, AppSdkRuntimeIssue> { 1642 SdkRelayTargetPolicy::try_explicit(relays, policy.into()) 1643 .map_err(|error| AppSdkRuntimeIssue::from_sdk_error(&error)) 1644 } 1645 1646 fn app_sdk_farm_receipt( 1647 receipt: FarmEnqueueReceipt, 1648 actor_pubkey: String, 1649 ) -> AppSdkWorkflowReceipt { 1650 AppSdkWorkflowReceipt { 1651 operation_kind: FARM_PUBLISH_OPERATION_KIND.to_owned(), 1652 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1653 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1654 outbox_operation_id: receipt.outbox_operation_id, 1655 outbox_event_id: receipt.outbox_event_id, 1656 state: sdk_mutation_state_key(receipt.state).to_owned(), 1657 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1658 actor_pubkey, 1659 } 1660 } 1661 1662 fn app_sdk_listing_receipt( 1663 receipt: ListingEnqueueReceipt, 1664 actor_pubkey: String, 1665 ) -> AppSdkWorkflowReceipt { 1666 AppSdkWorkflowReceipt { 1667 operation_kind: LISTING_PUBLISH_OPERATION_KIND.to_owned(), 1668 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1669 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1670 outbox_operation_id: receipt.outbox_operation_id, 1671 outbox_event_id: receipt.outbox_event_id, 1672 state: sdk_mutation_state_key(receipt.state).to_owned(), 1673 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1674 actor_pubkey, 1675 } 1676 } 1677 1678 fn app_sdk_order_submit_ack( 1679 receipt: OrderSubmitReceipt, 1680 actor_pubkey: String, 1681 ) -> AppSdkWorkflowReceipt { 1682 AppSdkWorkflowReceipt { 1683 operation_kind: ORDER_SUBMIT_OPERATION_KIND.to_owned(), 1684 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1685 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1686 outbox_operation_id: receipt.outbox_operation_id, 1687 outbox_event_id: receipt.outbox_event_id, 1688 state: sdk_mutation_state_key(receipt.state).to_owned(), 1689 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1690 actor_pubkey, 1691 } 1692 } 1693 1694 fn app_sdk_order_decision_receipt( 1695 receipt: OrderDecisionReceipt, 1696 actor_pubkey: String, 1697 ) -> AppSdkWorkflowReceipt { 1698 AppSdkWorkflowReceipt { 1699 operation_kind: ORDER_DECISION_OPERATION_KIND.to_owned(), 1700 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1701 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1702 outbox_operation_id: receipt.outbox_operation_id, 1703 outbox_event_id: receipt.outbox_event_id, 1704 state: sdk_mutation_state_key(receipt.state).to_owned(), 1705 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1706 actor_pubkey, 1707 } 1708 } 1709 1710 fn app_sdk_order_revision_proposal_receipt( 1711 receipt: OrderRevisionProposalReceipt, 1712 actor_pubkey: String, 1713 ) -> AppSdkWorkflowReceipt { 1714 AppSdkWorkflowReceipt { 1715 operation_kind: ORDER_REVISION_PROPOSAL_OPERATION_KIND.to_owned(), 1716 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1717 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1718 outbox_operation_id: receipt.outbox_operation_id, 1719 outbox_event_id: receipt.outbox_event_id, 1720 state: sdk_mutation_state_key(receipt.state).to_owned(), 1721 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1722 actor_pubkey, 1723 } 1724 } 1725 1726 fn app_sdk_order_revision_decision_receipt( 1727 receipt: OrderRevisionDecisionReceipt, 1728 actor_pubkey: String, 1729 ) -> AppSdkWorkflowReceipt { 1730 AppSdkWorkflowReceipt { 1731 operation_kind: ORDER_REVISION_DECISION_OPERATION_KIND.to_owned(), 1732 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1733 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1734 outbox_operation_id: receipt.outbox_operation_id, 1735 outbox_event_id: receipt.outbox_event_id, 1736 state: sdk_mutation_state_key(receipt.state).to_owned(), 1737 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1738 actor_pubkey, 1739 } 1740 } 1741 1742 fn app_sdk_order_cancellation_receipt( 1743 receipt: OrderCancellationReceipt, 1744 actor_pubkey: String, 1745 ) -> AppSdkWorkflowReceipt { 1746 AppSdkWorkflowReceipt { 1747 operation_kind: ORDER_CANCELLATION_OPERATION_KIND.to_owned(), 1748 expected_event_id: receipt.expected_event_id.as_str().to_owned(), 1749 signed_event_id: receipt.signed_event_id.as_str().to_owned(), 1750 outbox_operation_id: receipt.outbox_operation_id, 1751 outbox_event_id: receipt.outbox_event_id, 1752 state: sdk_mutation_state_key(receipt.state).to_owned(), 1753 idempotency_digest_prefix: receipt.idempotency_digest_prefix, 1754 actor_pubkey, 1755 } 1756 } 1757 1758 fn sdk_mutation_state_key(state: SdkMutationState) -> &'static str { 1759 match state { 1760 SdkMutationState::StoredAndQueued => "enqueued", 1761 SdkMutationState::AlreadyQueued => "already_queued", 1762 _ => "unknown", 1763 } 1764 } 1765 1766 fn send_worker_result<T>( 1767 shared: &AppSdkRuntimeShared, 1768 response_sender: mpsc::Sender<Result<T, AppSdkRuntimeIssue>>, 1769 result: Result<T, AppSdkRuntimeIssue>, 1770 ) { 1771 set_last_issue( 1772 shared, 1773 match &result { 1774 Ok(_) => None, 1775 Err(issue) => Some(issue.clone()), 1776 }, 1777 ); 1778 let _ = response_sender.send(result); 1779 } 1780 1781 fn lifecycle_busy_issue(shared: &AppSdkRuntimeShared) -> Option<AppSdkRuntimeIssue> { 1782 let state = lock_status(shared).state; 1783 if matches!( 1784 state, 1785 AppSdkLifecycleState::Pausing 1786 | AppSdkLifecycleState::Paused 1787 | AppSdkLifecycleState::Restoring 1788 | AppSdkLifecycleState::RebuildingProjections 1789 | AppSdkLifecycleState::ShuttingDown 1790 ) { 1791 Some(AppSdkRuntimeIssue::lifecycle_blocked(state)) 1792 } else { 1793 None 1794 } 1795 } 1796 1797 fn runtime_unavailable_issue(shared: &AppSdkRuntimeShared) -> AppSdkRuntimeIssue { 1798 let status = lock_status(shared).clone(); 1799 if let Some(issue) = status.last_issue { 1800 issue 1801 } else { 1802 AppSdkRuntimeIssue::runtime_error( 1803 "sdk_runtime_not_ready", 1804 format!("app sdk runtime is {:?}", status.state), 1805 ) 1806 } 1807 } 1808 1809 fn replace_status(shared: &AppSdkRuntimeShared, status: AppSdkRuntimeStatus) { 1810 *lock_status(shared) = status; 1811 shared.status_changed.notify_all(); 1812 } 1813 1814 fn set_last_issue(shared: &AppSdkRuntimeShared, issue: Option<AppSdkRuntimeIssue>) { 1815 lock_status(shared).last_issue = issue; 1816 shared.status_changed.notify_all(); 1817 } 1818 1819 fn transition_status_state(shared: &AppSdkRuntimeShared, state: AppSdkLifecycleState) { 1820 lock_status(shared).state = state; 1821 shared.status_changed.notify_all(); 1822 } 1823 1824 fn mark_projections_stale( 1825 shared: &AppSdkRuntimeShared, 1826 reason: impl Into<String>, 1827 restore_source: Option<PathBuf>, 1828 ) -> AppSdkProjectionLifecycleStatus { 1829 let mut status = lock_status(shared); 1830 status.projection_lifecycle = AppSdkProjectionLifecycleStatus::stale(reason, restore_source); 1831 status.state = AppSdkLifecycleState::Ready; 1832 let projection_lifecycle = status.projection_lifecycle.clone(); 1833 shared.status_changed.notify_all(); 1834 projection_lifecycle 1835 } 1836 1837 fn begin_projection_rebuild(shared: &AppSdkRuntimeShared) -> AppSdkProjectionLifecycleStatus { 1838 let restore_source = lock_status(shared) 1839 .projection_lifecycle 1840 .restore_source 1841 .clone(); 1842 let mut status = lock_status(shared); 1843 status.state = AppSdkLifecycleState::RebuildingProjections; 1844 status.projection_lifecycle = 1845 AppSdkProjectionLifecycleStatus::rebuilding("sdk_projection_rebuild", restore_source); 1846 let projection_lifecycle = status.projection_lifecycle.clone(); 1847 shared.status_changed.notify_all(); 1848 projection_lifecycle 1849 } 1850 1851 fn complete_projection_rebuild( 1852 shared: &AppSdkRuntimeShared, 1853 ) -> Result<AppSdkProjectionLifecycleStatus, AppSdkRuntimeIssue> { 1854 let mut status = lock_status(shared); 1855 if !matches!(status.state, AppSdkLifecycleState::RebuildingProjections) { 1856 return Err(AppSdkRuntimeIssue::lifecycle_blocked(status.state)); 1857 } 1858 status.state = AppSdkLifecycleState::Ready; 1859 status.projection_lifecycle = AppSdkProjectionLifecycleStatus::current(); 1860 let projection_lifecycle = status.projection_lifecycle.clone(); 1861 shared.status_changed.notify_all(); 1862 Ok(projection_lifecycle) 1863 } 1864 1865 fn lock_status(shared: &AppSdkRuntimeShared) -> MutexGuard<'_, AppSdkRuntimeStatus> { 1866 shared 1867 .status 1868 .lock() 1869 .unwrap_or_else(|poisoned| poisoned.into_inner()) 1870 } 1871 1872 fn sdk_error_class_label(error: &RadrootsSdkError) -> String { 1873 serde_json::to_value(error.class()) 1874 .ok() 1875 .and_then(|value| value.as_str().map(str::to_owned)) 1876 .unwrap_or_else(|| format!("{:?}", error.class())) 1877 } 1878 1879 fn serialized_label(value: &(impl Serialize + fmt::Debug)) -> String { 1880 serde_json::to_value(value) 1881 .ok() 1882 .and_then(|value| value.as_str().map(str::to_owned)) 1883 .unwrap_or_else(|| format!("{value:?}")) 1884 } 1885 1886 #[cfg(test)] 1887 mod tests { 1888 use std::{ 1889 fs, 1890 sync::{ 1891 Arc, Condvar, Mutex, 1892 atomic::{AtomicBool, Ordering}, 1893 mpsc, 1894 }, 1895 thread, 1896 time::{Duration, SystemTime, UNIX_EPOCH}, 1897 }; 1898 1899 use radroots_core::{ 1900 RadrootsCoreCurrency, RadrootsCoreDecimal, RadrootsCoreMoney, RadrootsCoreQuantity, 1901 RadrootsCoreQuantityPrice, RadrootsCoreUnit, 1902 }; 1903 use radroots_events::{ 1904 farm::RadrootsFarmRef, 1905 ids::{RadrootsDTag, RadrootsInventoryBinId}, 1906 listing::{ 1907 RadrootsListing, RadrootsListingAvailability, RadrootsListingBin, 1908 RadrootsListingDeliveryMethod, RadrootsListingLocation, RadrootsListingProduct, 1909 RadrootsListingStatus, 1910 }, 1911 }; 1912 use radroots_nostr::prelude::{RadrootsNostrKeys, RadrootsNostrSecretKey}; 1913 use radroots_sdk::{ 1914 BackupRequest, LISTING_PUBLISH_OPERATION_KIND, RadrootsSdk, 1915 SdkRelayUrlPolicy as SdkRuntimeRelayUrlPolicy, 1916 }; 1917 1918 use crate::{ 1919 APP_RUNTIME_NAMESPACE, AppDesktopRuntimePaths, AppRuntimeHostEnvironment, 1920 AppRuntimePlatform, 1921 }; 1922 1923 use super::{ 1924 APP_SDK_STORAGE_DIR_NAME, AppSdkConfig, AppSdkLifecycleState, AppSdkListingPublishRequest, 1925 AppSdkProjectionLifecycleState, AppSdkRelayUrlPolicy, AppSdkRestorePreflightRequest, 1926 AppSdkRuntime, AppSdkRuntimeError, AppSdkRuntimeShared, AppSdkRuntimeStatus, 1927 AppSdkWorkerCommand, app_sdk_storage_root_from_data_root, transition_status_state, 1928 }; 1929 1930 const SDK_TEST_SELLER_SECRET_KEY_HEX: &str = 1931 "10c5304d6c9ae3a1a16f7860f1cc8f5e3a76225a2663b3a989a0d775919b7df5"; 1932 1933 #[test] 1934 fn sdk_config_uses_app_data_sdk_storage_root() { 1935 let paths = AppDesktopRuntimePaths::for_desktop( 1936 AppRuntimePlatform::Macos, 1937 AppRuntimeHostEnvironment { 1938 home_dir: Some("/Users/treesap".into()), 1939 ..AppRuntimeHostEnvironment::default() 1940 }, 1941 ) 1942 .expect("desktop paths should resolve"); 1943 let config = 1944 AppSdkConfig::from_desktop_paths(&paths, vec!["wss://relay.example".to_owned()]); 1945 1946 assert_eq!( 1947 config.storage_root, 1948 paths.app.data.join(APP_SDK_STORAGE_DIR_NAME) 1949 ); 1950 assert_eq!( 1951 config.storage_root, 1952 app_sdk_storage_root_from_data_root(paths.app.data.as_path()) 1953 ); 1954 assert_eq!(config.storage_root.parent(), Some(paths.app.data.as_path())); 1955 assert!(paths.app.data.ends_with(APP_RUNTIME_NAMESPACE)); 1956 assert_eq!(config.relay_url_policy, AppSdkRelayUrlPolicy::Public); 1957 } 1958 1959 #[test] 1960 fn sdk_config_uses_localhost_policy_for_ws_relay_urls() { 1961 let config = AppSdkConfig::from_app_data_root( 1962 "/tmp/radroots-app-data".as_ref(), 1963 vec![ 1964 "wss://relay.example".to_owned(), 1965 "ws://127.0.0.1:8080".to_owned(), 1966 ], 1967 ); 1968 1969 assert_eq!(config.relay_url_policy, AppSdkRelayUrlPolicy::Localhost); 1970 } 1971 1972 #[test] 1973 fn sdk_runtime_reaches_ready_with_directory_storage() { 1974 let storage_root = temp_storage_root("ready"); 1975 let config = AppSdkConfig::from_app_data_root( 1976 storage_root 1977 .parent() 1978 .expect("storage root should have parent"), 1979 vec!["ws://127.0.0.1:8080".to_owned()], 1980 ); 1981 let runtime = AppSdkRuntime::start(config).expect("sdk runtime should start"); 1982 1983 let status = runtime.wait_for_startup(Duration::from_secs(5)); 1984 1985 assert_eq!(status.state, AppSdkLifecycleState::Ready); 1986 assert_eq!(status.storage_root, storage_root); 1987 assert_eq!(status.relay_url_policy, AppSdkRelayUrlPolicy::Localhost); 1988 let storage_paths = status 1989 .storage_paths 1990 .expect("storage paths should be present"); 1991 assert_eq!( 1992 storage_paths.event_store_path, 1993 storage_root.join("event_store.sqlite") 1994 ); 1995 assert_eq!( 1996 storage_paths.outbox_path, 1997 storage_root.join("outbox.sqlite") 1998 ); 1999 let storage = runtime 2000 .storage_status() 2001 .expect("storage diagnostics should load"); 2002 assert_eq!(storage.storage_kind, "directory"); 2003 assert!(storage.event_store.store.integrity_ok); 2004 assert!(storage.outbox.store.integrity_ok); 2005 let integrity = runtime 2006 .integrity_status() 2007 .expect("integrity diagnostics should load"); 2008 assert!(integrity.event_store_ok); 2009 assert!(integrity.outbox_ok); 2010 let sync = runtime.sync_status().expect("sync diagnostics should load"); 2011 assert_eq!(sync.source, "sdk_canonical_stores"); 2012 assert_eq!(sync.relay_targets.configured_count, 1); 2013 let diagnostics = runtime.diagnostics().expect("diagnostics should load"); 2014 assert_eq!(diagnostics.runtime.state, AppSdkLifecycleState::Ready); 2015 assert_eq!(diagnostics.storage.storage_kind, "directory"); 2016 assert_eq!(diagnostics.sync.relay_targets.configured_count, 1); 2017 runtime.shutdown().expect("sdk runtime should shut down"); 2018 assert_eq!(runtime.status().state, AppSdkLifecycleState::Stopped); 2019 let _ = fs::remove_dir_all(storage_root); 2020 } 2021 2022 #[test] 2023 fn sdk_runtime_enqueues_listing_publish_work() { 2024 let storage_root = temp_storage_root("listing_enqueue"); 2025 let config = AppSdkConfig::from_app_data_root( 2026 storage_root 2027 .parent() 2028 .expect("storage root should have parent"), 2029 vec!["ws://127.0.0.1:8080".to_owned()], 2030 ); 2031 let runtime = AppSdkRuntime::start(config).expect("sdk runtime should start"); 2032 assert_eq!( 2033 runtime.wait_for_startup(Duration::from_secs(5)).state, 2034 AppSdkLifecycleState::Ready 2035 ); 2036 let secret_key = RadrootsNostrSecretKey::from_hex(SDK_TEST_SELLER_SECRET_KEY_HEX) 2037 .expect("secret key should parse"); 2038 let signer_keys = RadrootsNostrKeys::new(secret_key); 2039 let seller_pubkey = signer_keys.public_key().to_hex(); 2040 2041 let receipt = runtime 2042 .enqueue_listing_publish(AppSdkListingPublishRequest { 2043 actor_account_id: "seller-account".to_owned(), 2044 actor_pubkey: seller_pubkey.clone(), 2045 signer_keys, 2046 listing: test_listing(seller_pubkey.as_str()), 2047 target_relays: vec!["ws://127.0.0.1:8080".to_owned()], 2048 relay_url_policy: AppSdkRelayUrlPolicy::Localhost, 2049 idempotency_key: Some("listing-enqueue-idempotency".to_owned()), 2050 }) 2051 .expect("listing publish should enqueue"); 2052 2053 assert_eq!(receipt.operation_kind, LISTING_PUBLISH_OPERATION_KIND); 2054 assert_eq!(receipt.actor_pubkey, seller_pubkey); 2055 assert_eq!(receipt.state, "enqueued"); 2056 assert!(!receipt.expected_event_id.is_empty()); 2057 assert_eq!(receipt.expected_event_id, receipt.signed_event_id); 2058 assert!(receipt.outbox_operation_id > 0); 2059 assert!(receipt.outbox_event_id > 0); 2060 assert!(receipt.idempotency_digest_prefix.is_some()); 2061 let sync = runtime.sync_status().expect("sync diagnostics should load"); 2062 assert_eq!(sync.outbox.ready_signed_events, 1); 2063 runtime.shutdown().expect("sdk runtime should shut down"); 2064 let _ = fs::remove_dir_all(storage_root); 2065 } 2066 2067 #[test] 2068 fn sdk_runtime_degrades_with_structured_sdk_error() { 2069 let storage_root = temp_storage_root("invalid_relay"); 2070 let config = AppSdkConfig::from_app_data_root( 2071 storage_root 2072 .parent() 2073 .expect("storage root should have parent"), 2074 vec!["ws://relay.example".to_owned()], 2075 ); 2076 let runtime = AppSdkRuntime::start(config).expect("sdk runtime should start"); 2077 2078 let status = runtime.wait_for_startup(Duration::from_secs(5)); 2079 2080 assert_eq!(status.state, AppSdkLifecycleState::Degraded); 2081 let issue = status 2082 .last_issue 2083 .expect("degraded status should include issue"); 2084 assert_eq!(issue.code, "invalid_relay_url"); 2085 assert_eq!(issue.class, "configuration"); 2086 assert!(!issue.retryable); 2087 assert!( 2088 issue 2089 .recovery_actions 2090 .contains(&"configure_relay_targets".to_owned()) 2091 ); 2092 assert_eq!(issue.detail_json["code"], "invalid_relay_url"); 2093 let error = runtime 2094 .diagnostics() 2095 .expect_err("degraded diagnostics should fail"); 2096 match error { 2097 AppSdkRuntimeError::CommandFailed(issue) => { 2098 assert_eq!(issue.code, "invalid_relay_url"); 2099 assert_eq!(issue.class, "configuration"); 2100 assert_eq!(issue.detail_json["code"], "invalid_relay_url"); 2101 } 2102 unexpected => panic!("unexpected degraded diagnostics error: {unexpected:?}"), 2103 } 2104 runtime.shutdown().expect("sdk runtime should shut down"); 2105 let _ = fs::remove_dir_all(storage_root); 2106 } 2107 2108 #[test] 2109 fn sdk_shutdown_joins_when_normal_command_queue_is_full() { 2110 let config = AppSdkConfig::from_app_data_root( 2111 "/tmp/radroots-app-sdk-full-queue".as_ref(), 2112 vec!["ws://127.0.0.1:8080".to_owned()], 2113 ) 2114 .with_command_queue_capacity(1); 2115 let shared = Arc::new(AppSdkRuntimeShared { 2116 status: Mutex::new(AppSdkRuntimeStatus::from_config( 2117 &config, 2118 AppSdkLifecycleState::Ready, 2119 None, 2120 None, 2121 )), 2122 status_changed: Condvar::new(), 2123 shutdown_requested: AtomicBool::new(false), 2124 }); 2125 let (command_sender, command_receiver) = mpsc::sync_channel(config.command_queue_capacity); 2126 let worker_shared = Arc::clone(&shared); 2127 let worker = thread::spawn(move || { 2128 while !worker_shared.shutdown_requested.load(Ordering::SeqCst) { 2129 thread::sleep(Duration::from_millis(1)); 2130 } 2131 drop(command_receiver); 2132 transition_status_state(&worker_shared, AppSdkLifecycleState::Stopped); 2133 }); 2134 let runtime = AppSdkRuntime { 2135 command_sender: Mutex::new(Some(command_sender)), 2136 shared, 2137 worker: Mutex::new(Some(worker)), 2138 }; 2139 let (response_sender, _response_receiver) = mpsc::channel(); 2140 runtime 2141 .command_sender 2142 .lock() 2143 .expect("command sender lock") 2144 .as_ref() 2145 .expect("command sender") 2146 .try_send(AppSdkWorkerCommand::Diagnostics(response_sender)) 2147 .expect("normal command queue should fill"); 2148 2149 assert!(matches!( 2150 runtime.sync_status(), 2151 Err(AppSdkRuntimeError::CommandQueueFull) 2152 )); 2153 assert_eq!(runtime.status().state, AppSdkLifecycleState::Ready); 2154 2155 runtime 2156 .shutdown() 2157 .expect("shutdown should not depend on normal command queue capacity"); 2158 2159 assert_eq!(runtime.status().state, AppSdkLifecycleState::Stopped); 2160 } 2161 2162 #[test] 2163 fn sdk_restore_preflight_marks_projections_stale_without_writing_destination() { 2164 let backup_source_root = temp_storage_root("restore_backup_source"); 2165 let backup_archive = backup_source_root 2166 .parent() 2167 .expect("backup source should have parent") 2168 .join("backup_archive"); 2169 let tokio = tokio::runtime::Builder::new_current_thread() 2170 .enable_all() 2171 .build() 2172 .expect("tokio runtime"); 2173 let sdk = tokio 2174 .block_on( 2175 RadrootsSdk::builder() 2176 .directory_storage(backup_source_root.clone()) 2177 .relay_url_policy(SdkRuntimeRelayUrlPolicy::Localhost) 2178 .relay_url("ws://127.0.0.1:8080") 2179 .build(), 2180 ) 2181 .expect("source sdk should build"); 2182 tokio 2183 .block_on(sdk.backup(BackupRequest::new(backup_archive.clone()))) 2184 .expect("backup should complete"); 2185 2186 let app_storage_root = temp_storage_root("restore_preflight_destination"); 2187 let app_data_root = app_storage_root 2188 .parent() 2189 .expect("app storage root should have parent") 2190 .to_path_buf(); 2191 let config = AppSdkConfig::from_app_data_root( 2192 app_data_root.as_path(), 2193 vec!["ws://127.0.0.1:8080".to_owned()], 2194 ); 2195 let runtime = AppSdkRuntime::start(config).expect("sdk runtime should start"); 2196 assert_eq!( 2197 runtime.wait_for_startup(Duration::from_secs(5)).state, 2198 AppSdkLifecycleState::Ready 2199 ); 2200 let sentinel = app_storage_root.join("restore-preflight-sentinel"); 2201 fs::write(&sentinel, "existing destination").expect("sentinel should write"); 2202 2203 let receipt = runtime 2204 .restore_preflight( 2205 AppSdkRestorePreflightRequest::new(backup_archive.clone()) 2206 .with_overwrite_existing_sdk_storage(true), 2207 ) 2208 .expect("restore preflight should succeed"); 2209 2210 assert_eq!(receipt.state, "dry_run"); 2211 assert_eq!(receipt.destination, app_storage_root); 2212 assert_eq!(receipt.restored_paths, None); 2213 assert!(sentinel.exists()); 2214 assert_eq!( 2215 receipt.projection_lifecycle.state, 2216 AppSdkProjectionLifecycleState::Stale 2217 ); 2218 assert_eq!( 2219 receipt.projection_lifecycle.reason.as_deref(), 2220 Some("sdk_restore_preflight") 2221 ); 2222 assert_eq!( 2223 runtime.status().projection_lifecycle.state, 2224 AppSdkProjectionLifecycleState::Stale 2225 ); 2226 assert_eq!(runtime.status().state, AppSdkLifecycleState::Ready); 2227 runtime.shutdown().expect("sdk runtime should shut down"); 2228 let _ = fs::remove_dir_all( 2229 backup_source_root 2230 .parent() 2231 .expect("backup source should have parent"), 2232 ); 2233 let _ = fs::remove_dir_all(app_data_root); 2234 } 2235 2236 #[test] 2237 fn sdk_projection_rebuild_state_rejects_conflicting_commands() { 2238 let storage_root = temp_storage_root("projection_rebuild"); 2239 let config = AppSdkConfig::from_app_data_root( 2240 storage_root 2241 .parent() 2242 .expect("storage root should have parent"), 2243 vec!["ws://127.0.0.1:8080".to_owned()], 2244 ); 2245 let runtime = AppSdkRuntime::start(config).expect("sdk runtime should start"); 2246 assert_eq!( 2247 runtime.wait_for_startup(Duration::from_secs(5)).state, 2248 AppSdkLifecycleState::Ready 2249 ); 2250 2251 let rebuilding = runtime 2252 .begin_projection_rebuild() 2253 .expect("projection rebuild should start"); 2254 2255 assert_eq!(rebuilding.state, AppSdkProjectionLifecycleState::Rebuilding); 2256 assert_eq!( 2257 runtime.status().state, 2258 AppSdkLifecycleState::RebuildingProjections 2259 ); 2260 let error = runtime 2261 .sync_status() 2262 .expect_err("sync status should wait for rebuild completion"); 2263 match error { 2264 AppSdkRuntimeError::CommandFailed(issue) => { 2265 assert_eq!(issue.code, "sdk_lifecycle_busy"); 2266 assert_eq!(issue.detail_json["state"], "RebuildingProjections"); 2267 } 2268 unexpected => panic!("unexpected lifecycle error: {unexpected:?}"), 2269 } 2270 2271 let complete = runtime 2272 .complete_projection_rebuild() 2273 .expect("projection rebuild should complete"); 2274 2275 assert_eq!(complete.state, AppSdkProjectionLifecycleState::Current); 2276 assert_eq!(runtime.status().state, AppSdkLifecycleState::Ready); 2277 runtime 2278 .sync_status() 2279 .expect("sync status should work after rebuild"); 2280 runtime.shutdown().expect("sdk runtime should shut down"); 2281 let _ = fs::remove_dir_all(storage_root); 2282 } 2283 2284 fn temp_storage_root(label: &str) -> std::path::PathBuf { 2285 let nanos = SystemTime::now() 2286 .duration_since(UNIX_EPOCH) 2287 .expect("clock") 2288 .as_nanos(); 2289 std::env::temp_dir() 2290 .join(format!("radroots_app_sdk_runtime_{label}_{nanos}")) 2291 .join(APP_SDK_STORAGE_DIR_NAME) 2292 } 2293 2294 fn test_listing(seller_pubkey: &str) -> RadrootsListing { 2295 let bin_id = RadrootsInventoryBinId::parse("bin-1").expect("bin id"); 2296 RadrootsListing { 2297 d_tag: RadrootsDTag::parse("AAAAAAAAAAAAAAAAAAAAAQ").expect("d tag"), 2298 published_at: None, 2299 farm: RadrootsFarmRef { 2300 pubkey: seller_pubkey.to_owned(), 2301 d_tag: "AAAAAAAAAAAAAAAAAAAAAA".to_owned(), 2302 }, 2303 product: RadrootsListingProduct { 2304 key: "coffee".to_owned(), 2305 title: "Coffee".to_owned(), 2306 category: "coffee".to_owned(), 2307 summary: Some("Single origin coffee".to_owned()), 2308 process: None, 2309 lot: None, 2310 location: None, 2311 profile: None, 2312 year: None, 2313 }, 2314 primary_bin_id: bin_id.clone(), 2315 bins: vec![RadrootsListingBin { 2316 bin_id, 2317 quantity: RadrootsCoreQuantity::new( 2318 RadrootsCoreDecimal::from(1000u32), 2319 RadrootsCoreUnit::MassG, 2320 ), 2321 price_per_canonical_unit: RadrootsCoreQuantityPrice { 2322 amount: RadrootsCoreMoney::new( 2323 RadrootsCoreDecimal::from(20u32), 2324 RadrootsCoreCurrency::USD, 2325 ), 2326 quantity: RadrootsCoreQuantity::new( 2327 RadrootsCoreDecimal::from(1u32), 2328 RadrootsCoreUnit::MassG, 2329 ), 2330 }, 2331 display_amount: None, 2332 display_unit: None, 2333 display_label: None, 2334 display_price: None, 2335 display_price_unit: None, 2336 }], 2337 resource_area: None, 2338 plot: None, 2339 discounts: None, 2340 inventory_available: Some(RadrootsCoreDecimal::from(5u32)), 2341 availability: Some(RadrootsListingAvailability::Status { 2342 status: RadrootsListingStatus::Active, 2343 }), 2344 delivery_method: Some(RadrootsListingDeliveryMethod::Pickup), 2345 location: Some(RadrootsListingLocation { 2346 primary: "North Farm".to_owned(), 2347 city: None, 2348 region: None, 2349 country: Some("US".to_owned()), 2350 lat: None, 2351 lng: None, 2352 geohash: None, 2353 }), 2354 images: None, 2355 } 2356 } 2357 }