sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

commit 140ee41c9956bc1121f6d424342e4643e2af4c3e
parent 2397de02f145089dd8d335fd6d91daf61ebddcc9
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 22:34:37 -0700

sdk: separate listing observation time

Diffstat:
Mcrates/sdk/src/listings_runtime.rs | 12++++++------
Mcrates/sdk/src/runtime.rs | 9+++++++++
Mcrates/sdk/src/sync_runtime.rs | 11+----------
Mcrates/sdk/tests/listings_runtime.rs | 53+++++++++++++++++++++++++++++++++++++++++++++++++++--
4 files changed, 67 insertions(+), 18 deletions(-)

diff --git a/crates/sdk/src/listings_runtime.rs b/crates/sdk/src/listings_runtime.rs @@ -1,7 +1,7 @@ #[cfg(feature = "runtime")] use crate::{ ListingsClient, RadrootsSdkError, RadrootsSdkTimestamp, SdkIdempotencyKey, - SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, + SdkRelayTargetPolicy, SdkRelayTargetSet, SdkRelayUrlPolicy, runtime::sdk_now_ms, }; #[cfg(feature = "runtime")] use radroots_authority::{RadrootsActorContext, RadrootsEventSigner, sign_authorized_draft}; @@ -145,16 +145,16 @@ pub struct ListingPublishPlan { #[cfg(feature = "runtime")] #[derive(Clone, Debug, PartialEq, Eq)] pub enum SdkMutationState { - Inserted, - Existing, + StoredAndQueued, + AlreadyQueued, } #[cfg(feature = "runtime")] impl From<RadrootsOutboxEnqueueStatus> for SdkMutationState { fn from(value: RadrootsOutboxEnqueueStatus) -> Self { match value { - RadrootsOutboxEnqueueStatus::Inserted => Self::Inserted, - RadrootsOutboxEnqueueStatus::Existing => Self::Existing, + RadrootsOutboxEnqueueStatus::Inserted => Self::StoredAndQueued, + RadrootsOutboxEnqueueStatus::Existing => Self::AlreadyQueued, } } } @@ -230,7 +230,7 @@ impl<'sdk> ListingsClient<'sdk> { target_relays.relays(), )?, }; - let observed_at_ms = i64::from(plan.frozen_draft.created_at) * 1_000; + let observed_at_ms = sdk_now_ms(self.sdk)?; let signed_event_id = parse_event_id(signed_event.id.as_str(), "signed event id")?; let event = event_from_signed(&signed_event); let ingest = RadrootsEventIngest::new(event, observed_at_ms) diff --git a/crates/sdk/src/runtime.rs b/crates/sdk/src/runtime.rs @@ -192,6 +192,15 @@ impl RadrootsSdk { } #[cfg(feature = "runtime")] +pub(crate) fn sdk_now_ms(sdk: &RadrootsSdk) -> Result<i64, RadrootsSdkError> { + let seconds = sdk.now()?.unix_seconds(); + let millis = seconds + .checked_mul(1_000) + .ok_or(RadrootsSdkError::TimestampOutOfRange { value: seconds })?; + i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds }) +} + +#[cfg(feature = "runtime")] struct OpenedRuntimeStorage { event_store: RadrootsEventStore, outbox: RadrootsOutbox, diff --git a/crates/sdk/src/sync_runtime.rs b/crates/sdk/src/sync_runtime.rs @@ -1,5 +1,5 @@ #[cfg(feature = "runtime")] -use crate::{RadrootsSdkError, SyncClient}; +use crate::{RadrootsSdkError, SyncClient, runtime::sdk_now_ms}; #[cfg(all(feature = "runtime", feature = "relay-runtime"))] use radroots_nostr::prelude::RadrootsNostrClient; #[cfg(feature = "runtime")] @@ -268,15 +268,6 @@ impl<'sdk> SyncClient<'sdk> { } #[cfg(feature = "runtime")] -fn sdk_now_ms(sdk: &crate::RadrootsSdk) -> Result<i64, RadrootsSdkError> { - let seconds = sdk.now()?.unix_seconds(); - let millis = seconds - .checked_mul(1_000) - .ok_or(RadrootsSdkError::TimestampOutOfRange { value: seconds })?; - i64::try_from(millis).map_err(|_| RadrootsSdkError::TimestampOutOfRange { value: seconds }) -} - -#[cfg(feature = "runtime")] fn push_outbox_claim_token() -> String { format!("radroots-sdk-sync-{}", uuid::Uuid::now_v7()) } diff --git a/crates/sdk/tests/listings_runtime.rs b/crates/sdk/tests/listings_runtime.rs @@ -36,6 +36,7 @@ const LISTING_G_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAABw"; const LISTING_H_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACA"; const LISTING_I_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACQ"; const LISTING_J_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACg"; +const LISTING_K_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAACw"; const RELAY: &str = "wss://relay.example.com"; const RELAY_B: &str = "wss://relay-b.example.com"; @@ -250,7 +251,7 @@ async fn enqueue_publish_stores_event_and_queues_signed_outbox_without_publish() assert_eq!(receipt.local_event_seq, 1); assert_eq!(receipt.outbox_operation_id, 1); assert_eq!(receipt.outbox_event_id, 1); - assert_eq!(receipt.state, SdkMutationState::Inserted); + assert_eq!(receipt.state, SdkMutationState::StoredAndQueued); assert!(receipt.idempotency_digest_prefix.is_some()); let paths = sdk.storage_paths().expect("paths"); @@ -422,6 +423,54 @@ async fn enqueue_prepared_publish_returns_sanitized_signer_errors() { } #[tokio::test] +async fn explicit_historical_created_at_does_not_backdate_observed_at_ms() { + let (_tempdir, sdk) = directory_sdk().await; + let created_at = RadrootsSdkTimestamp::from_unix_seconds(1_600_000_000); + let observed_at_ms = 1_700_000_000_000; + let request = ListingEnqueuePublishRequest::new( + actor(), + listing(LISTING_K_D_TAG, "Coffee"), + SdkRelayTargetPolicy::UseConfiguredRelays, + ) + .with_created_at(created_at); + + let receipt = sdk + .listings() + .enqueue_publish(request, &FixtureSigner::new(SELLER)) + .await + .expect("enqueue"); + + let paths = sdk.storage_paths().expect("paths"); + let event_store = RadrootsEventStore::open_file(&paths.event_store_path) + .await + .expect("event store"); + let stored_event = event_store + .get_event(receipt.signed_event_id.as_str()) + .await + .expect("event lookup") + .expect("stored event"); + assert_eq!(stored_event.created_at, 1_600_000_000); + assert_eq!(stored_event.inserted_at_ms, observed_at_ms); + assert_eq!(stored_event.updated_at_ms, observed_at_ms); + + let outbox = RadrootsOutbox::open_file(&paths.outbox_path) + .await + .expect("outbox"); + let outbox_event = outbox + .get_event(receipt.outbox_event_id) + .await + .expect("outbox event") + .expect("outbox event"); + assert_eq!(outbox_event.draft.created_at, 1_600_000_000); + assert_eq!( + outbox_event.event_store_ingested_at_ms, + Some(observed_at_ms) + ); + assert_eq!(outbox_event.created_at_ms, observed_at_ms); + assert_eq!(outbox_event.updated_at_ms, observed_at_ms); +} + +#[tokio::test] async fn enqueue_publish_returns_sanitized_signer_errors() { let (_tempdir, sdk) = directory_sdk().await; let request = ListingEnqueuePublishRequest::new( @@ -524,5 +573,5 @@ async fn enqueue_publish_derives_order_independent_idempotency_key() { first_receipt.idempotency_digest_prefix, second_receipt.idempotency_digest_prefix ); - assert_eq!(second_receipt.state, SdkMutationState::Existing); + assert_eq!(second_receipt.state, SdkMutationState::AlreadyQueued); }