app

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

commit bf304895261f36cccb6ceddf970fb7fbb414e7c3
parent 1be8e001f8e065164af6b1ff88a65c10eb2c8214
Author: triesap <tyson@radroots.org>
Date:   Sun,  9 Aug 2026 19:10:37 +0000

ffi: rename the HarvestCircle cross-language API

- rename exported core, error, and observer types across Rust and Kotlin
- align runtime threads, properties, development paths, and probe variables
- refresh the normalized FFI contract digest
- verify checks and native bindings through both build lanes

Diffstat:
Mapp/desktop/build.gradle.kts | 6+++---
Mapp/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleAppStore.kt | 4++--
Mapp/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleApplication.kt | 10+++++-----
Mapp/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleCoreGateway.kt | 14+++++++-------
Mapp/desktop/src/main/kotlin/org/radroots/harvestcircle/application/NativeCompatibility.kt | 2+-
Mapp/desktop/src/test/kotlin/org/radroots/harvestcircle/application/NativeGeneratedRecoveryTest.kt | 6+++---
Mcore/crates/harvestcircle_ffi/src/commands.rs | 142++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcore/crates/harvestcircle_ffi/src/lib.rs | 6+++---
Mcore/crates/harvestcircle_ffi/src/observer.rs | 49+++++++++++++++++++++++++++----------------------
Mcore/crates/harvestcircle_nostr/src/client.rs | 2+-
Mcore/crates/harvestcircle_storage/src/db.rs | 4++--
11 files changed, 126 insertions(+), 119 deletions(-)

diff --git a/app/desktop/build.gradle.kts b/app/desktop/build.gradle.kts @@ -663,7 +663,7 @@ tasks.named("runKtlintFormatOverMainSourceSet") { tasks.withType<Test>().configureEach { dependsOn(buildRustCoreDebug) environment( - "RADROOTS_STUDIO_DEVELOPMENT_DATA_DIR", + "HARVESTCIRCLE_DEVELOPMENT_DATA_DIR", layout.buildDirectory .dir("native-test-data") .get() @@ -676,7 +676,7 @@ tasks.withType<Test>().configureEach { } tasks.withType<JavaExec>().configureEach { dependsOn(buildRustCoreDebug) - systemProperty("radroots.studio.development", "true") + systemProperty("harvestcircle.development", "true") systemProperty( "jna.library.path", rustDebugLibrary.parentFile.absolutePath, @@ -713,7 +713,7 @@ compose.desktop { packageName = applicationName packageVersion = installableVersion - description = "Radroots Studio $appVersion" + description = "Radroots HarvestCircle $appVersion" copyright = "Copyright © 2024 Radroots, Inc." vendor = "Radroots, Inc" diff --git a/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleAppStore.kt b/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleAppStore.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.radroots.harvestcircle.ffi.AppLifecycleDto import org.radroots.harvestcircle.ffi.AppSnapshotDto -import org.radroots.harvestcircle.ffi.StudioException +import org.radroots.harvestcircle.ffi.HarvestCircleException import org.radroots.harvestcircle.ffi.WireErrorCode import org.radroots.harvestcircle.ffi.WireRecoveryAction @@ -435,7 +435,7 @@ class HarvestCircleAppStore( } private fun acceptFailure(error: Throwable) { - val native = error as? StudioException.Failure + val native = error as? HarvestCircleException.Failure val gatewayFailure = (error as? HarvestCircleGatewayException)?.failure mutableState.value = mutableState.value.copy( diff --git a/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleApplication.kt b/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleApplication.kt @@ -9,8 +9,8 @@ import org.radroots.harvestcircle.accounts.ui.HarvestCircleScreen import org.radroots.harvestcircle.accounts.ui.HarvestCircleUiActions import org.radroots.harvestcircle.accounts.ui.StartupFailureScreen import org.radroots.harvestcircle.accounts.ui.toUiModel -import org.radroots.harvestcircle.ffi.StudioAppCore -import org.radroots.harvestcircle.ffi.StudioException +import org.radroots.harvestcircle.ffi.HarvestCircleAppCore +import org.radroots.harvestcircle.ffi.HarvestCircleException import org.radroots.harvestcircle.ffi.compatibilityDescriptor internal typealias HarvestCircleStoreFactory = (CoroutineScope) -> HarvestCircleAppStore @@ -23,7 +23,7 @@ fun HarvestCircleApplication(storeFactory: HarvestCircleStoreFactory = ::createH if (store == null) { val error = storeResult.exceptionOrNull() val message = - (error as? StudioException.Failure)?.safeMessage + (error as? HarvestCircleException.Failure)?.safeMessage ?: "The application could not start." StartupFailureScreen(message) return @@ -65,10 +65,10 @@ fun HarvestCircleApplication(storeFactory: HarvestCircleStoreFactory = ::createH } internal fun createHarvestCircleAppStore(scope: CoroutineScope): HarvestCircleAppStore { - val developmentMode = java.lang.Boolean.getBoolean("radroots.studio.development") + val developmentMode = java.lang.Boolean.getBoolean("harvestcircle.development") val descriptor = compatibilityDescriptor() val core = - StudioAppCore.openCompatible( + HarvestCircleAppCore.openCompatible( expectation = verifyNativeCompatibility(descriptor), developmentMode = developmentMode, ) diff --git a/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleCoreGateway.kt b/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/HarvestCircleCoreGateway.kt @@ -5,13 +5,13 @@ import kotlinx.coroutines.runBlocking import org.radroots.harvestcircle.ffi.AccountDto import org.radroots.harvestcircle.ffi.AppSnapshotDto import org.radroots.harvestcircle.ffi.GeneratedRecoveryRequest +import org.radroots.harvestcircle.ffi.HarvestCircleAppCore +import org.radroots.harvestcircle.ffi.HarvestCircleChangeObserver +import org.radroots.harvestcircle.ffi.HarvestCircleException import org.radroots.harvestcircle.ffi.ObserverSubscription import org.radroots.harvestcircle.ffi.RemovalRequest import org.radroots.harvestcircle.ffi.RequestContextDto import org.radroots.harvestcircle.ffi.SnapshotChangeDto -import org.radroots.harvestcircle.ffi.StudioAppCore -import org.radroots.harvestcircle.ffi.StudioChangeObserver -import org.radroots.harvestcircle.ffi.StudioException import org.radroots.harvestcircle.ffi.WireErrorCategory import org.radroots.harvestcircle.ffi.WireErrorCode import org.radroots.harvestcircle.ffi.WireRecoveryAction @@ -107,7 +107,7 @@ interface HarvestCircleCoreGateway : AutoCloseable { } class NativeHarvestCircleCoreGateway( - private val core: StudioAppCore, + private val core: HarvestCircleAppCore, ) : HarvestCircleCoreGateway { private val nextRequest = AtomicLong(1) private val shutdownLock = Any() @@ -118,7 +118,7 @@ class NativeHarvestCircleCoreGateway( override suspend fun subscribeChanges(onChange: (HarvestCircleChange) -> Unit): AutoCloseable { val subscription = core.subscribeChangesV2( - object : StudioChangeObserver { + object : HarvestCircleChangeObserver { override fun onChange(change: SnapshotChangeDto) { onChange(HarvestCircleChange(change.snapshot, change.previousRevision)) } @@ -219,7 +219,7 @@ internal fun Throwable.toHarvestCircleCommandFailure( fallbackCorrelationId: String, fallbackSafeMessage: String = "The application command failed.", ): HarvestCircleCommandFailure { - val native = this as? StudioException.Failure + val native = this as? HarvestCircleException.Failure return HarvestCircleCommandFailure( code = native?.code ?: WireErrorCode.INTERNAL, category = native?.category ?: WireErrorCategory.INTERNAL, @@ -256,7 +256,7 @@ private class NativeRemovalTicket( } private class NativeGeneratedRecoveryTicket( - private val core: StudioAppCore, + private val core: HarvestCircleAppCore, private val request: GeneratedRecoveryRequest, private val requestContext: () -> RequestContextDto, override val requestId: String, diff --git a/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/NativeCompatibility.kt b/app/desktop/src/main/kotlin/org/radroots/harvestcircle/application/NativeCompatibility.kt @@ -5,7 +5,7 @@ import org.radroots.harvestcircle.ffi.CompatibilityExpectation internal const val EXPECTED_PRODUCT_VERSION = "0.1.0-alpha" internal const val EXPECTED_CARGO_PACKAGE_VERSION = "0.1.0-alpha" -internal const val EXPECTED_FFI_CONTRACT_HASH = "249b8f9ec8ce64e33d5b88dd52d7dc646a2d5b6993c738e71253964531d6d210" +internal const val EXPECTED_FFI_CONTRACT_HASH = "d4e298f0abeaa65aa68e70d7a6e8f69f8182f12f93c12b2dd056d3ed5d83e9c0" internal val EXPECTED_FFI_CONTRACT_MAJOR: UShort = 3.toUShort() internal val MINIMUM_FFI_CONTRACT_MINOR: UShort = 0.toUShort() internal const val MINIMUM_STORAGE_SCHEMA: UInt = 5U diff --git a/app/desktop/src/test/kotlin/org/radroots/harvestcircle/application/NativeGeneratedRecoveryTest.kt b/app/desktop/src/test/kotlin/org/radroots/harvestcircle/application/NativeGeneratedRecoveryTest.kt @@ -1,7 +1,7 @@ package org.radroots.harvestcircle.application import kotlinx.coroutines.test.runTest -import org.radroots.harvestcircle.ffi.StudioAppCore +import org.radroots.harvestcircle.ffi.HarvestCircleAppCore import org.radroots.harvestcircle.ffi.compatibilityDescriptor import java.nio.file.Files import java.nio.file.Path @@ -14,7 +14,7 @@ import kotlin.test.assertTrue class NativeGeneratedRecoveryTest { private val dataDirectory = - Path.of(checkNotNull(System.getenv("RADROOTS_STUDIO_DEVELOPMENT_DATA_DIR"))) + Path.of(checkNotNull(System.getenv("HARVESTCIRCLE_DEVELOPMENT_DATA_DIR"))) @BeforeTest fun prepareDataDirectory() { @@ -31,7 +31,7 @@ class NativeGeneratedRecoveryTest { fun generatedRecoveryCrossesTheNativeBoundaryAndCancelsWithoutPersistence() = runTest { val core = - StudioAppCore.openCompatible( + HarvestCircleAppCore.openCompatible( expectation = verifyNativeCompatibility(compatibilityDescriptor()), developmentMode = true, ) diff --git a/core/crates/harvestcircle_ffi/src/commands.rs b/core/crates/harvestcircle_ffi/src/commands.rs @@ -31,7 +31,7 @@ const DATABASE_QUALIFIER: &str = "org"; const DATABASE_ORGANIZATION: &str = "radroots"; const DATABASE_APPLICATION: &str = "studio"; const DATABASE_FILENAME: &str = "studio.sqlite3"; -const DEVELOPMENT_DATA_DIR_ENVIRONMENT: &str = "RADROOTS_STUDIO_DEVELOPMENT_DATA_DIR"; +const DEVELOPMENT_DATA_DIR_ENVIRONMENT: &str = "HARVESTCIRCLE_DEVELOPMENT_DATA_DIR"; pub(crate) const ACTOR_MAILBOX_CAPACITY: usize = 64; const MAX_COMMAND_DEADLINE_MILLIS: u64 = 30_000; @@ -88,7 +88,7 @@ pub fn compatibility_descriptor() -> CompatibilityDescriptor { #[derive(Debug)] #[cfg_attr(not(coverage_nightly), derive(uniffi::Error))] -pub enum StudioError { +pub enum HarvestCircleError { Failure { code: WireErrorCode, category: WireErrorCategory, @@ -99,7 +99,7 @@ pub enum StudioError { }, } -impl Display for StudioError { +impl Display for HarvestCircleError { fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result { match self { Self::Failure { safe_message, .. } => formatter.write_str(safe_message), @@ -107,9 +107,9 @@ impl Display for StudioError { } } -impl std::error::Error for StudioError {} +impl std::error::Error for HarvestCircleError {} -impl From<SafeError> for StudioError { +impl From<SafeError> for HarvestCircleError { fn from(error: SafeError) -> Self { let (category, retryable, recovery_action) = error_policy(error.code()); Self::Failure { @@ -123,7 +123,7 @@ impl From<SafeError> for StudioError { } } -impl StudioError { +impl HarvestCircleError { fn correlated(error: SafeError, correlation_id: &str) -> Self { let (category, retryable, recovery_action) = error_policy(error.code()); Self::Failure { @@ -158,11 +158,11 @@ impl GeneratedRecoveryRequest { /// # Errors /// /// Returns a safe unavailable error after the first read. - pub fn take_recovery_nsec(&self) -> Result<String, StudioError> { + pub fn take_recovery_nsec(&self) -> Result<String, HarvestCircleError> { self.handle .take_recovery_nsec() .map(|nsec| nsec.with_exposed_secret(str::to_owned)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } } @@ -230,12 +230,12 @@ impl RuntimeCore { } #[cfg_attr(not(coverage_nightly), derive(uniffi::Object))] -pub struct StudioAppCore { +pub struct HarvestCircleAppCore { pub(crate) inner: Arc<RuntimeCore>, } #[cfg_attr(not(coverage_nightly), uniffi::export)] -impl StudioAppCore { +impl HarvestCircleAppCore { /// Verifies the static contract before touching the application data path. /// /// # Errors @@ -246,7 +246,7 @@ impl StudioAppCore { pub fn open_compatible( expectation: CompatibilityExpectation, development_mode: bool, - ) -> Result<Arc<Self>, StudioError> { + ) -> Result<Arc<Self>, HarvestCircleError> { let path = application_database_path(development_mode)?; Self::open_path_compatible(&path, &expectation, development_mode) } @@ -256,13 +256,13 @@ impl StudioAppCore { /// # Errors /// /// Returns a safe storage, recovery, or application-state error. - pub async fn bootstrap(&self) -> Result<AppSnapshotDto, StudioError> { + pub async fn bootstrap(&self) -> Result<AppSnapshotDto, HarvestCircleError> { self.inner .actor .bootstrap() .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } #[must_use] @@ -277,7 +277,7 @@ impl StudioAppCore { /// Returns a safe key-generation, conflict, timeout, or lifecycle error. pub async fn begin_generated_account_v2( &self, - ) -> Result<Arc<GeneratedRecoveryRequest>, StudioError> { + ) -> Result<Arc<GeneratedRecoveryRequest>, HarvestCircleError> { self.inner .actor .begin_generated_key_stage() @@ -288,7 +288,7 @@ impl StudioAppCore { resolved: AtomicBool::new(false), }) }) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Acknowledges recovery and commits the generated account once. @@ -301,12 +301,12 @@ impl StudioAppCore { &self, context: RequestContextDto, request: Arc<GeneratedRecoveryRequest>, - ) -> Result<AppSnapshotDto, StudioError> { + ) -> Result<AppSnapshotDto, HarvestCircleError> { if request.resolved.swap(true, Ordering::AcqRel) { return Err(generated_recovery_expired()); } let request_id = DurableRequestId::parse(context.request_id.clone()) - .map_err(|error| StudioError::correlated(error, &context.request_id))?; + .map_err(|error| HarvestCircleError::correlated(error, &context.request_id))?; let timeout = command_timeout(context.deadline_millis, &context.request_id)?; self.inner .actor @@ -329,7 +329,7 @@ impl StudioAppCore { pub async fn cancel_generated_account_v2( &self, request: Arc<GeneratedRecoveryRequest>, - ) -> Result<bool, StudioError> { + ) -> Result<bool, HarvestCircleError> { if request.resolved.swap(true, Ordering::AcqRel) { return Ok(false); } @@ -337,7 +337,7 @@ impl StudioAppCore { .actor .cancel_generated_key_stage() .await - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Imports or repairs an account using a caller-owned idempotency key. @@ -349,12 +349,12 @@ impl StudioAppCore { &self, context: RequestContextDto, secret_key: Vec<u8>, - ) -> Result<AccountCommandReceiptDto, StudioError> { + ) -> Result<AccountCommandReceiptDto, HarvestCircleError> { let request_id = DurableRequestId::parse(context.request_id.clone()) - .map_err(|error| StudioError::correlated(error, &context.request_id))?; + .map_err(|error| HarvestCircleError::correlated(error, &context.request_id))?; let timeout = command_timeout(context.deadline_millis, &context.request_id)?; let input = SecretKeyInput::parse_bytes(secret_key) - .map_err(|error| StudioError::correlated(error, &context.request_id))?; + .map_err(|error| HarvestCircleError::correlated(error, &context.request_id))?; self.inner .actor .import_secret_key( @@ -372,7 +372,7 @@ impl StudioAppCore { snapshot, } }) - .map_err(|error| StudioError::correlated(error, &context.request_id)) + .map_err(|error| HarvestCircleError::correlated(error, &context.request_id)) } /// Selects one saved account without activating it. @@ -383,14 +383,14 @@ impl StudioAppCore { pub async fn select_account( &self, public_key_hex: String, - ) -> Result<AppSnapshotDto, StudioError> { + ) -> Result<AppSnapshotDto, HarvestCircleError> { let public_key = parse_public_key(&public_key_hex)?; self.inner .actor .select_account(public_key) .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Activates one saved account after validating its credential. @@ -401,14 +401,14 @@ impl StudioAppCore { pub async fn activate_account( &self, public_key_hex: String, - ) -> Result<AppSnapshotDto, StudioError> { + ) -> Result<AppSnapshotDto, HarvestCircleError> { let public_key = parse_public_key(&public_key_hex)?; self.inner .actor .activate_account(public_key) .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Signs out while retaining accounts and credentials. @@ -416,13 +416,13 @@ impl StudioAppCore { /// # Errors /// /// Returns a safe application-state error. - pub async fn sign_out(&self) -> Result<AppSnapshotDto, StudioError> { + pub async fn sign_out(&self) -> Result<AppSnapshotDto, HarvestCircleError> { self.inner .actor .sign_out() .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Refreshes the active Nostr profile from configured relays. @@ -430,13 +430,13 @@ impl StudioAppCore { /// # Errors /// /// Returns a safe storage or application-state error. - pub async fn refresh_active_profile(&self) -> Result<AppSnapshotDto, StudioError> { + pub async fn refresh_active_profile(&self) -> Result<AppSnapshotDto, HarvestCircleError> { self.inner .actor .refresh_active_profile() .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Issues a revision-bound removal confirmation object. @@ -447,7 +447,7 @@ impl StudioAppCore { pub async fn request_account_removal( &self, public_key_hex: String, - ) -> Result<Arc<RemovalRequest>, StudioError> { + ) -> Result<Arc<RemovalRequest>, HarvestCircleError> { let public_key = parse_public_key(&public_key_hex)?; self.inner .actor @@ -463,7 +463,7 @@ impl StudioAppCore { token: Mutex::new(Some(token)), }) }) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } /// Permanently removes the account represented by a one-time request. @@ -475,7 +475,7 @@ impl StudioAppCore { &self, context: RequestContextDto, request: Arc<RemovalRequest>, - ) -> Result<AppSnapshotDto, StudioError> { + ) -> Result<AppSnapshotDto, HarvestCircleError> { let token = request .token .lock() @@ -483,7 +483,7 @@ impl StudioAppCore { .take() .ok_or_else(confirmation_expired)?; let request_id = DurableRequestId::parse(context.request_id.clone()) - .map_err(|error| StudioError::correlated(error, &context.request_id))?; + .map_err(|error| HarvestCircleError::correlated(error, &context.request_id))?; let timeout = command_timeout(context.deadline_millis, &context.request_id)?; self.inner .actor @@ -495,11 +495,11 @@ impl StudioAppCore { ) .await .map(|snapshot| self.inner.dto_for(&snapshot)) - .map_err(StudioError::from) + .map_err(HarvestCircleError::from) } } -fn verify_compatibility(expectation: &CompatibilityExpectation) -> Result<(), StudioError> { +fn verify_compatibility(expectation: &CompatibilityExpectation) -> Result<(), HarvestCircleError> { let actual = compatibility_descriptor(); if expectation.contract_major != actual.contract_major || expectation.minimum_contract_minor > actual.contract_minor @@ -512,12 +512,12 @@ fn verify_compatibility(expectation: &CompatibilityExpectation) -> Result<(), St Ok(()) } -impl StudioAppCore { +impl HarvestCircleAppCore { fn open_path_compatible( path: &Path, expectation: &CompatibilityExpectation, development_mode: bool, - ) -> Result<Arc<Self>, StudioError> { + ) -> Result<Arc<Self>, HarvestCircleError> { verify_compatibility(expectation)?; std::fs::create_dir_all(path.parent().ok_or_else(path_unavailable)?) .map_err(|_| path_unavailable())?; @@ -528,7 +528,7 @@ impl StudioAppCore { // SQLite ownership. Platform installation lanes exercise this adapter; // deterministic coverage owns the compatibility and runtime policies. #[cfg_attr(coverage_nightly, coverage(off))] - fn open_path(path: &Path, development_mode: bool) -> Result<Arc<Self>, StudioError> { + fn open_path(path: &Path, development_mode: bool) -> Result<Arc<Self>, HarvestCircleError> { let mode = if development_mode { RelayRuntimeMode::Development } else { @@ -585,7 +585,7 @@ impl Clock for SystemClock { // ProjectDirs and the process environment are host integration boundaries. #[cfg_attr(coverage_nightly, coverage(off))] -fn application_database_path(development_mode: bool) -> Result<PathBuf, StudioError> { +fn application_database_path(development_mode: bool) -> Result<PathBuf, HarvestCircleError> { if development_mode && let Some(directory) = std::env::var_os(DEVELOPMENT_DATA_DIR_ENVIRONMENT) { return Ok(PathBuf::from(directory).join(DATABASE_FILENAME)); @@ -599,13 +599,13 @@ fn application_database_path(development_mode: bool) -> Result<PathBuf, StudioEr .ok_or_else(path_unavailable) } -fn parse_public_key(value: &str) -> Result<PublicKey, StudioError> { - PublicKey::from_hex(value).map_err(StudioError::from) +fn parse_public_key(value: &str) -> Result<PublicKey, HarvestCircleError> { + PublicKey::from_hex(value).map_err(HarvestCircleError::from) } -fn command_timeout(millis: u64, correlation_id: &str) -> Result<Duration, StudioError> { +fn command_timeout(millis: u64, correlation_id: &str) -> Result<Duration, HarvestCircleError> { if millis == 0 || millis > MAX_COMMAND_DEADLINE_MILLIS { - return Err(StudioError::Failure { + return Err(HarvestCircleError::Failure { code: WireErrorCode::InvalidApplicationState, category: WireErrorCategory::Input, retryable: false, @@ -617,13 +617,13 @@ fn command_timeout(millis: u64, correlation_id: &str) -> Result<Duration, Studio Ok(Duration::from_millis(millis)) } -pub(crate) fn runtime() -> Result<&'static tokio::runtime::Runtime, StudioError> { +pub(crate) fn runtime() -> Result<&'static tokio::runtime::Runtime, HarvestCircleError> { static RUNTIME: OnceLock<Result<tokio::runtime::Runtime, ()>> = OnceLock::new(); RUNTIME .get_or_init(|| { tokio::runtime::Builder::new_multi_thread() .enable_all() - .thread_name("radroots-studio-core") + .thread_name("harvestcircle-core") .build() .map_err(|_| ()) }) @@ -631,12 +631,12 @@ pub(crate) fn runtime() -> Result<&'static tokio::runtime::Runtime, StudioError> .map_err(|()| runtime_unavailable()) } -fn actor_mailbox_capacity() -> Result<NonZeroUsize, StudioError> { +fn actor_mailbox_capacity() -> Result<NonZeroUsize, HarvestCircleError> { NonZeroUsize::new(ACTOR_MAILBOX_CAPACITY).ok_or_else(runtime_unavailable) } -fn runtime_unavailable() -> StudioError { - StudioError::Failure { +fn runtime_unavailable() -> HarvestCircleError { + HarvestCircleError::Failure { code: WireErrorCode::InvalidApplicationState, category: WireErrorCategory::Lifecycle, retryable: true, @@ -646,8 +646,8 @@ fn runtime_unavailable() -> StudioError { } } -fn path_unavailable() -> StudioError { - StudioError::Failure { +fn path_unavailable() -> HarvestCircleError { + HarvestCircleError::Failure { code: WireErrorCode::StorageUnavailable, category: WireErrorCategory::Storage, retryable: true, @@ -657,8 +657,8 @@ fn path_unavailable() -> StudioError { } } -fn confirmation_expired() -> StudioError { - StudioError::Failure { +fn confirmation_expired() -> HarvestCircleError { + HarvestCircleError::Failure { code: WireErrorCode::InvalidApplicationState, category: WireErrorCategory::Lifecycle, retryable: false, @@ -668,8 +668,8 @@ fn confirmation_expired() -> StudioError { } } -fn generated_recovery_expired() -> StudioError { - StudioError::Failure { +fn generated_recovery_expired() -> HarvestCircleError { + HarvestCircleError::Failure { code: WireErrorCode::InvalidApplicationState, category: WireErrorCategory::Lifecycle, retryable: false, @@ -679,9 +679,9 @@ fn generated_recovery_expired() -> StudioError { } } -fn generated_commit_failed(error: SafeError) -> StudioError { +fn generated_commit_failed(error: SafeError) -> HarvestCircleError { let (category, _, _) = error_policy(error.code()); - StudioError::Failure { + HarvestCircleError::Failure { code: error.code().into(), category, retryable: false, @@ -693,8 +693,8 @@ fn generated_commit_failed(error: SafeError) -> StudioError { } } -fn compatibility_mismatch() -> StudioError { - StudioError::Failure { +fn compatibility_mismatch() -> HarvestCircleError { + HarvestCircleError::Failure { code: WireErrorCode::CompatibilityMismatch, category: WireErrorCategory::Compatibility, retryable: false, @@ -722,14 +722,14 @@ mod tests { use super::{ ACTOR_MAILBOX_CAPACITY, CompatibilityExpectation, DATABASE_APPLICATION, DATABASE_FILENAME, DATABASE_ORGANIZATION, DATABASE_QUALIFIER, FFI_CONTRACT_HASH, FFI_CONTRACT_MAJOR, - FFI_CONTRACT_MINOR, RequestContextDto, RuntimeCore, StudioAppCore, StudioError, - SystemClock, WireErrorCategory, WireErrorCode, WireRecoveryAction, actor_mailbox_capacity, - compatibility_descriptor, confirmation_expired, generated_commit_failed, - local_first_relay_configuration, path_unavailable, runtime, runtime_unavailable, - verify_compatibility, + FFI_CONTRACT_MINOR, HarvestCircleAppCore, HarvestCircleError, RequestContextDto, + RuntimeCore, SystemClock, WireErrorCategory, WireErrorCode, WireRecoveryAction, + actor_mailbox_capacity, compatibility_descriptor, confirmation_expired, + generated_commit_failed, local_first_relay_configuration, path_unavailable, runtime, + runtime_unavailable, verify_compatibility, }; - async fn in_memory_core() -> Arc<StudioAppCore> { + async fn in_memory_core() -> Arc<HarvestCircleAppCore> { let actor = RuntimeActorHandle::in_memory( RelayConfiguration::default(), RuntimeDependencies::new( @@ -743,7 +743,7 @@ mod tests { ) .await .expect("in-memory actor"); - Arc::new(StudioAppCore { + Arc::new(HarvestCircleAppCore { inner: Arc::new(RuntimeCore { actor, observers: std::sync::Mutex::new(std::collections::BTreeMap::new()), @@ -816,7 +816,7 @@ mod tests { .expect_err("repeated acknowledgement"); assert!(matches!( repeated, - StudioError::Failure { safe_message, .. } + HarvestCircleError::Failure { safe_message, .. } if safe_message == "The generated-key recovery step is no longer valid." )); } @@ -991,7 +991,7 @@ mod tests { assert_eq!(error.to_string(), message); assert!(matches!( error, - StudioError::Failure { + HarvestCircleError::Failure { code: actual_code, category: actual_category, retryable: actual_retryable, @@ -1050,7 +1050,9 @@ mod tests { contract_major: FFI_CONTRACT_MAJOR + 1, ..compatible }; - assert!(StudioAppCore::open_path_compatible(&rejected, &incompatible, true).is_err()); + assert!( + HarvestCircleAppCore::open_path_compatible(&rejected, &incompatible, true).is_err() + ); assert!(!rejected.parent().expect("parent").exists()); } diff --git a/core/crates/harvestcircle_ffi/src/lib.rs b/core/crates/harvestcircle_ffi/src/lib.rs @@ -7,8 +7,8 @@ mod dto; mod observer; pub use commands::{ - AccountCommandReceiptDto, GeneratedRecoveryRequest, RemovalRequest, RequestContextDto, - StudioAppCore, StudioError, + AccountCommandReceiptDto, GeneratedRecoveryRequest, HarvestCircleAppCore, HarvestCircleError, + RemovalRequest, RequestContextDto, }; pub use contract::{ FFI_CONTRACT_HASH, FFI_CONTRACT_MAJOR, FFI_CONTRACT_MINOR, MINIMUM_SCHEMA_VERSION, @@ -20,7 +20,7 @@ pub use dto::{ WireErrorCategory, WireErrorCode, WireRecoveryAction, }; pub use observer::{ - ObserverSubscription, ShutdownReceiptDto, SnapshotChangeDto, StudioChangeObserver, + HarvestCircleChangeObserver, ObserverSubscription, ShutdownReceiptDto, SnapshotChangeDto, }; uniffi::setup_scaffolding!(); diff --git a/core/crates/harvestcircle_ffi/src/observer.rs b/core/crates/harvestcircle_ffi/src/observer.rs @@ -6,7 +6,7 @@ use std::sync::{Arc, Mutex, Weak}; use harvestcircle_application::ChangeSubscriptionId; use crate::commands::RuntimeCore; -use crate::{AppSnapshotDto, StudioAppCore, StudioError}; +use crate::{AppSnapshotDto, HarvestCircleAppCore, HarvestCircleError}; const OBSERVER_CHANGE_CAPACITY: NonZeroUsize = NonZeroUsize::MIN.saturating_add(63); const MAX_OBSERVERS: usize = 32; @@ -26,7 +26,7 @@ pub struct ShutdownReceiptDto { } #[cfg_attr(not(coverage_nightly), uniffi::export(callback_interface))] -pub trait StudioChangeObserver: Send + Sync { +pub trait HarvestCircleChangeObserver: Send + Sync { fn on_change(&self, change: SnapshotChangeDto); } @@ -62,7 +62,7 @@ impl ObserverSubscription { } #[cfg_attr(not(coverage_nightly), uniffi::export)] -impl StudioAppCore { +impl HarvestCircleAppCore { /// Subscribes to ordered revision changes including predecessor metadata. /// /// # Errors @@ -70,8 +70,8 @@ impl StudioAppCore { /// Returns a safe observer or lifecycle error. pub async fn subscribe_changes_v2( &self, - observer: Box<dyn StudioChangeObserver>, - ) -> Result<Arc<ObserverSubscription>, StudioError> { + observer: Box<dyn HarvestCircleChangeObserver>, + ) -> Result<Arc<ObserverSubscription>, HarvestCircleError> { if self.inner.closed.load(Ordering::Acquire) { return Err(closed_error()); } @@ -80,9 +80,9 @@ impl StudioAppCore { .actor .subscribe_changes(OBSERVER_CHANGE_CAPACITY) .await - .map_err(StudioError::from)?; + .map_err(HarvestCircleError::from)?; let id = subscription.id(); - let observer: Arc<dyn StudioChangeObserver> = Arc::from(observer); + let observer: Arc<dyn HarvestCircleChangeObserver> = Arc::from(observer); let runtime_core = Arc::downgrade(&self.inner); let admitted = { let mut observers = self @@ -102,7 +102,7 @@ impl StudioAppCore { .actor .unsubscribe_changes(id) .await - .map_err(StudioError::from)?; + .map_err(HarvestCircleError::from)?; return Err(observer_registration_error()); } let task = crate::commands::runtime()?.spawn(async move { @@ -161,7 +161,7 @@ impl StudioAppCore { /// # Errors /// /// Returns a safe closed or timeout error when shutdown cannot complete. - pub async fn shutdown_v2(&self) -> Result<ShutdownReceiptDto, StudioError> { + pub async fn shutdown_v2(&self) -> Result<ShutdownReceiptDto, HarvestCircleError> { if self.inner.closed.swap(true, Ordering::AcqRel) { return Err(closed_error()); } @@ -178,7 +178,11 @@ impl StudioAppCore { let _ = task.await; } } - self.inner.actor.close().await.map_err(StudioError::from)?; + self.inner + .actor + .close() + .await + .map_err(HarvestCircleError::from)?; Ok(ShutdownReceiptDto { final_revision: self.inner.actor.snapshot().revision().value(), closed: true, @@ -186,8 +190,8 @@ impl StudioAppCore { } } -fn closed_error() -> StudioError { - StudioError::Failure { +fn closed_error() -> HarvestCircleError { + HarvestCircleError::Failure { code: crate::WireErrorCode::InvalidApplicationState, category: crate::WireErrorCategory::Lifecycle, retryable: false, @@ -197,8 +201,8 @@ fn closed_error() -> StudioError { } } -fn observer_registration_error() -> StudioError { - StudioError::Failure { +fn observer_registration_error() -> HarvestCircleError { + HarvestCircleError::Failure { code: crate::WireErrorCode::ObserverRegistrationFailed, category: crate::WireErrorCategory::Lifecycle, retryable: true, @@ -227,7 +231,8 @@ mod tests { use crate::commands::{ACTOR_MAILBOX_CAPACITY, RuntimeCore, SystemClock, runtime}; use crate::{ - AppSnapshotDto, ProfileLoadStateDto, SnapshotChangeDto, StudioAppCore, StudioChangeObserver, + AppSnapshotDto, HarvestCircleAppCore, HarvestCircleChangeObserver, ProfileLoadStateDto, + SnapshotChangeDto, }; const SECRET_HEX: &str = "7e7e9c42a91bfef19fa7ea99d52d8afdb67d893a8fefba1f5cb9793f2107f6d7"; @@ -235,18 +240,18 @@ mod tests { #[derive(Default)] struct RecordingObserver { snapshots: Mutex<Vec<AppSnapshotDto>>, - core: Mutex<Option<Arc<StudioAppCore>>>, + core: Mutex<Option<Arc<HarvestCircleAppCore>>>, } struct PanickingObserver; - impl StudioChangeObserver for PanickingObserver { + impl HarvestCircleChangeObserver for PanickingObserver { fn on_change(&self, _change: SnapshotChangeDto) { panic!("injected host callback failure"); } } - impl StudioChangeObserver for RecordingObserver { + impl HarvestCircleChangeObserver for RecordingObserver { fn on_change(&self, change: SnapshotChangeDto) { let snapshot = change.snapshot; if let Some(core) = self.core.lock().expect("core").as_ref() { @@ -256,11 +261,11 @@ mod tests { } } - async fn core() -> Arc<StudioAppCore> { + async fn core() -> Arc<HarvestCircleAppCore> { core_with_relays(RelayConfiguration::default()).await } - async fn core_with_relays(relays: RelayConfiguration) -> Arc<StudioAppCore> { + async fn core_with_relays(relays: RelayConfiguration) -> Arc<HarvestCircleAppCore> { let actor = RuntimeActorHandle::in_memory( relays, RuntimeDependencies::new( @@ -274,7 +279,7 @@ mod tests { ) .await .expect("actor"); - Arc::new(StudioAppCore { + Arc::new(HarvestCircleAppCore { inner: Arc::new(RuntimeCore { actor, observers: Mutex::new(std::collections::BTreeMap::new()), @@ -471,7 +476,7 @@ mod tests { struct ArcObserver(Arc<RecordingObserver>); - impl StudioChangeObserver for ArcObserver { + impl HarvestCircleChangeObserver for ArcObserver { fn on_change(&self, change: SnapshotChangeDto) { self.0.on_change(change); } diff --git a/core/crates/harvestcircle_nostr/src/client.rs b/core/crates/harvestcircle_nostr/src/client.rs @@ -75,7 +75,7 @@ impl NostrClient for SdkNostrClient { .collect::<Result<Vec<_>, _>>() .map_err(|_| invalid_relay_configuration())?; let request = FetchRequest::new( - format!("studio-profile-{policy:?}"), + format!("harvestcircle-profile-{policy:?}"), TargetSet::new(targets).map_err(|_| invalid_relay_configuration())?, FetchBounds::new( MAX_PROFILE_EVENTS_PER_RELAY as u16, diff --git a/core/crates/harvestcircle_storage/src/db.rs b/core/crates/harvestcircle_storage/src/db.rs @@ -775,7 +775,7 @@ mod tests { .arg("--exact") .arg("db::tests::writable_ownership_child_probe") .arg("--nocapture") - .env("RADROOTS_STUDIO_LOCK_PROBE_PATH", &path) + .env("HARVESTCIRCLE_LOCK_PROBE_PATH", &path) .status() .expect("child process"); assert!(status.success()); @@ -783,7 +783,7 @@ mod tests { #[test] fn writable_ownership_child_probe() { - let Ok(path) = std::env::var("RADROOTS_STUDIO_LOCK_PROBE_PATH") else { + let Ok(path) = std::env::var("HARVESTCIRCLE_LOCK_PROBE_PATH") else { return; }; assert!(Database::open(Path::new(&path)).is_err());