commit 140ee41c9956bc1121f6d424342e4643e2af4c3e
parent 2397de02f145089dd8d335fd6d91daf61ebddcc9
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 22:34:37 -0700
sdk: separate listing observation time
Diffstat:
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);
}