KvStore

Trait KvStore 

Source
pub trait KvStore: Send + Sync {
    // Required methods
    fn put<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        arid: &'life1 ARID,
        envelope: &'life2 Envelope,
        ttl_seconds: Option<u64>,
        verbose: bool,
    ) -> Pin<Box<dyn Future<Output = Result<String>> + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;
    fn get<'life0, 'life1, 'async_trait>(
        &'life0 self,
        arid: &'life1 ARID,
        timeout_seconds: Option<u64>,
        verbose: bool,
    ) -> Pin<Box<dyn Future<Output = Result<Option<Envelope>>> + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
    fn exists<'life0, 'life1, 'async_trait>(
        &'life0 self,
        arid: &'life1 ARID,
    ) -> Pin<Box<dyn Future<Output = Result<bool>> + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait;
}
Expand description

Unified trait for key-value storage backends using ARID-based addressing.

All implementations provide write-once semantics: once an envelope is stored at an ARID, subsequent attempts to write to the same ARID will fail with an AlreadyExists error.

§Security Model

  • ARID holder can read (by deriving storage key)
  • ARID creator can write once (by deriving storage key)
  • Storage networks see only derived keys, never ARIDs themselves
  • ARIDs shared only via secure channels (GSTP, Signal, QR codes)

§Implementations

  • MainlineDhtKv: Fast, lightweight DHT storage (≤1 KB messages)
  • IpfsKv: Large capacity, content-addressed storage (up to 10 MB messages)
  • HybridKv: Automatic optimization by size, combining DHT speed with IPFS capacity

§Thread Safety

The KvStore trait requires Send + Sync, meaning implementations can be safely shared across threads. However, the futures returned by async methods are not required to be Send (note the ?Send bound on async_trait).

What this means in practice:

  • ✓ You can share a KvStore instance across threads
  • ✓ You can call methods and await them on any thread
  • ✓ Multiple threads can perform concurrent operations
  • ✗ You cannot move an in-flight future to another thread

Working pattern:

use std::sync::Arc;

let store = Arc::new(IpfsKv::new("http://127.0.0.1:5001"));

// Spawn threads that each do async work locally
let store1 = Arc::clone(&store);
let handle = std::thread::spawn(move || {
    tokio::runtime::Runtime::new().unwrap().block_on(async {
        let arid = ARID::new();
        let env = Envelope::new("data");
        store1.put(&arid, &env, None, false).await
    })
});

Non-working pattern:

let store = IpfsKv::new("http://127.0.0.1:5001");
let arid = ARID::new();
let env = Envelope::new("data");

// ERROR: Cannot spawn !Send future across threads
tokio::spawn(async move {
    store.put(&arid, &env, None, false).await
});

This limitation comes from underlying network client libraries and is typical for async I/O code. It does not prevent concurrent operations - each thread simply needs to .await its own futures locally.

Required Methods§

Source

fn put<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, arid: &'life1 ARID, envelope: &'life2 Envelope, ttl_seconds: Option<u64>, verbose: bool, ) -> Pin<Box<dyn Future<Output = Result<String>> + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Store an envelope at the given ARID.

§Write-Once Semantics

This operation will fail if the ARID already exists. The implementation must check for existence before writing and return an appropriate error if the key is already present.

§Parameters
  • arid: Cryptographic identifier for this storage location
  • envelope: The envelope to store
  • ttl_seconds: Optional time-to-live in seconds. After this time, the envelope may be removed from storage.
    • Mainline DHT: Ignored (no TTL support)
    • IPFS: Used as IPNS record lifetime (default: 24h if None)
    • Server: Clamped to max_ttl if exceeded; uses max_ttl if None. All entries expire (hubert is for coordination, not long-term storage).
  • verbose: If true, log operations with timestamps
§Returns

A receipt containing storage metadata on success, or an error if:

  • The ARID already exists (AlreadyExists)
  • The envelope is too large for this backend
  • Network operation fails
  • Serialization fails
§Example
let arid = ARID::new();
let envelope = Envelope::new("Hello, Hubert!");

// Store without TTL
let receipt = store.put(&arid, &envelope, None, false).await.unwrap();

// Store with 1 hour TTL and verbose logging
let arid2 = ARID::new();
let receipt2 = store
    .put(&arid2, &envelope, Some(3600), true)
    .await
    .unwrap();
println!("Stored at: {}", receipt2);
Source

fn get<'life0, 'life1, 'async_trait>( &'life0 self, arid: &'life1 ARID, timeout_seconds: Option<u64>, verbose: bool, ) -> Pin<Box<dyn Future<Output = Result<Option<Envelope>>> + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Retrieve an envelope for the given ARID.

Polls the storage backend until the envelope becomes available or the timeout is reached. This is useful for coordinating between parties where one party puts data and another polls for it.

§Parameters
  • arid: The ARID to look up
  • timeout_seconds: Maximum time to wait for the envelope to appear. If None, uses a backend-specific default (typically 30 seconds). After timeout, returns Ok(None) rather than continuing to poll.
  • verbose: If true, log operations with timestamps and print polling dots
§Returns
  • Ok(Some(envelope)) if found within the timeout
  • Ok(None) if not found after timeout expires
  • Err(_) on network or deserialization errors
§Example
// Wait up to 10 seconds for envelope to appear with verbose logging
match store.get(arid, Some(10), true).await.unwrap() {
    Some(envelope) => println!("Found: {}", envelope),
    None => println!("Not found within timeout"),
}
Source

fn exists<'life0, 'life1, 'async_trait>( &'life0 self, arid: &'life1 ARID, ) -> Pin<Box<dyn Future<Output = Result<bool>> + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait,

Check if an envelope exists at the given ARID.

§Parameters
  • arid: The ARID to check
§Returns
  • Ok(true) if the ARID exists
  • Ok(false) if the ARID does not exist
  • Err(_) on network errors
§Implementation Note

For hybrid storage, this only checks the DHT layer. Reference envelopes count as existing even if the referenced IPFS content is not available.

§Example
if store.exists(arid).await.unwrap() {
    println!("ARID already used");
} else {
    println!("ARID available");
}

Implementors§