heddle-thread-api 0.25.4

Native Thread clients and durable peer replication over Iroh
Documentation
#![cfg(feature = "replication")]

use api::v2::{
    MethodDescriptor,
    client::{Client, MessageReader, MessageWriter, RpcTransport},
};
use crypto::{Ed25519Signer, Signer};
use heddle_object_model::object::{
    StateId,
    thread_replication::{GENESIS_FORMAT, ThreadGenesis},
};
use heddle_thread_api::{
    Remote, Rpc, contract::*, creation::ThreadCreation, rpc, transport::Error,
};
use prost::Message;

struct Reply {
    request: StartThreadRequest,
    response: ThreadMutationResponse,
}
struct NoStream;
impl MessageReader for NoStream {
    type Error = Error;
    async fn next(&mut self) -> Result<Option<Vec<u8>>, Error> {
        panic!("creation is unary")
    }
    fn cancel(&mut self) {}
}
impl MessageWriter for NoStream {
    type Error = Error;
    async fn send(&mut self, _: Vec<u8>) -> Result<(), Error> {
        panic!("creation is unary")
    }
    async fn finish(&mut self) -> Result<(), Error> {
        panic!("creation is unary")
    }
    fn abort(&mut self) {}
}
impl RpcTransport for Reply {
    type Error = Error;
    type Reader = NoStream;
    type Writer = NoStream;
    async fn unary(
        &self,
        method: &'static MethodDescriptor,
        bytes: Vec<u8>,
    ) -> Result<Vec<u8>, Error> {
        assert_eq!(method.path, rpc::ThreadServiceStartThread::METHOD.path);
        assert_eq!(
            StartThreadRequest::decode(bytes.as_slice()).expect("request"),
            self.request
        );
        Ok(self.response.encode_to_vec())
    }
    async fn observe(&self, _: &'static MethodDescriptor, _: Vec<u8>) -> Result<NoStream, Error> {
        panic!("creation is unary")
    }
    async fn exchange(
        &self,
        _: &'static MethodDescriptor,
        _: Vec<u8>,
    ) -> Result<(NoStream, NoStream), Error> {
        panic!("creation is unary")
    }
}

#[tokio::test]
async fn creation_returns_a_bound_receipt_and_rejects_incomplete_or_unrelated_results() {
    let signer = Ed25519Signer::from_seed(&[11; 32]).expect("creator");
    let creation = ThreadCreation::sign(
        "01980000-0000-7000-8000-000000000002",
        &ThreadGenesis {
            version: 1,
            spool: "01980000-0000-7000-8000-000000000001".into(),
            parent: None,
            base: StateId::from_bytes([17; 32]),
            name: "work".into(),
            intent: "create once".into(),
            creator: signer.public_key().try_into().expect("key"),
            owner: heddle_object_model::object::thread_replication::GenesisOwner::LocalKey(
                signer.public_key().try_into().expect("key"),
            ),
            nonce: vec![23; 16],
        },
        &signer,
    )
    .expect("prepare");
    let description = DescribeEndpointResponse {
        endpoint: Some(EndpointRef {
            public_key: vec![7; 32],
            kind: EndpointKind::Weft as i32,
        }),
        understood_signed_record_formats: vec![GENESIS_FORMAT.into()],
        ..Default::default()
    };
    let valid = ThreadMutationResponse {
        receipt: Some(MutationReceipt {
            client_operation_id: creation.request().client_operation_id.clone(),
            endpoint: description.endpoint.clone(),
            outcome: Some(mutation_receipt::Outcome::Applied(Default::default())),
            ..Default::default()
        }),
        thread: Some(ThreadOverview {
            r#ref: Some(creation.reference().clone()),
            ..Default::default()
        }),
    };
    for case in 0..9 {
        let mut response = valid.clone();
        match case {
            0 => {}
            1 => response.receipt = None,
            2 => {
                response
                    .receipt
                    .as_mut()
                    .expect("receipt")
                    .client_operation_id = "other operation".into()
            }
            3 => {
                response
                    .receipt
                    .as_mut()
                    .expect("receipt")
                    .endpoint
                    .as_mut()
                    .expect("endpoint")
                    .public_key[0] ^= 1
            }
            4 => response.receipt.as_mut().expect("receipt").outcome = None,
            5 => {
                response
                    .thread
                    .as_mut()
                    .expect("overview")
                    .r#ref
                    .as_mut()
                    .expect("ref")
                    .id
                    .as_mut()
                    .expect("id")
                    .value[0] ^= 1
            }
            6 => response.thread = None,
            7 => {
                response.thread = None;
                response.receipt.as_mut().expect("receipt").outcome =
                    Some(mutation_receipt::Outcome::Blocked(Default::default()));
            }
            8 => {
                response.thread = None;
                response.receipt.as_mut().expect("receipt").outcome = Some(
                    mutation_receipt::Outcome::PendingOperation(Default::default()),
                );
            }
            _ => unreachable!(),
        }
        let remote = Remote {
            api: Client::new(
                Reply {
                    request: creation.request().clone(),
                    response: response.clone(),
                },
                [rpc::ThreadServiceStartThread::METHOD.path.into()],
            ),
            description: description.clone(),
        };
        let result = remote.start_thread(&creation).await;
        if matches!(case, 0 | 7 | 8) {
            assert_eq!(result.expect("typed outcome"), response);
        } else {
            assert!(result.is_err(), "invalid response accepted: case {case}");
        }
    }
}