1use spiral_contract::call::CallError;
3
4use crate::api::tables::objects::TableId;
5use crate::api::tables::{CreateVersionRequest, TablesError};
6use crate::resources::arbor::Client as ArborClient;
7use crate::scandal::types::{PreparedWriteReceipt, TableWriteResult};
8use crate::{Client, Error, Table};
9
10impl Client {
11 pub async fn register_prepared(&self, receipt: PreparedWriteReceipt) -> Result<Table, Error> {
16 let result = register(&self.connection.arbor, receipt).await?;
17 Ok(self.table_by_id(result.resource_id, result.version_id))
18 }
19}
20
21pub(crate) async fn register(
22 client: &ArborClient,
23 receipt: PreparedWriteReceipt,
24) -> Result<TableWriteResult, Error> {
25 let result = client
26 .create_version(
27 TableId(receipt.resource_id),
28 &CreateVersionRequest {
29 base_version_id: receipt.base_version,
30 version_id: receipt.version_id,
31 },
32 )
33 .await
34 .map_err(|error| match &error {
35 Error::Arbor(failure) if registration_may_have_committed(failure) => {
36 Error::RegistrationUnknown {
37 receipt: receipt.clone(),
38 message: error.to_string(),
39 }
40 }
41 _ => error,
42 })?;
43 if result.version_id != receipt.version_id
44 || result.parent_version_id != receipt.base_version
45 || result.seq_no.0 != receipt.sequence.0
46 {
47 return Err(Error::RegistrationUnknown {
48 receipt,
49 message: "registration did not match the prepared receipt".into(),
50 });
51 }
52 Ok(TableWriteResult {
53 resource_id: receipt.resource_id,
54 version_id: receipt.version_id,
55 })
56}
57
58fn registration_may_have_committed(error: &CallError<TablesError>) -> bool {
59 match error {
60 CallError::Service(failure) => failure.status() >= 500,
61 CallError::UnknownRemote(failure) => failure.status() >= 500,
62 CallError::Transport(failure) => failure.observed_status().is_none_or(uncertain_status),
63 CallError::Protocol(failure) => failure.observed_status().is_none_or(uncertain_status),
64 CallError::Internal { .. } => true,
65 }
66}
67
68fn uncertain_status(status: u16) -> bool {
69 (200..300).contains(&status) || status >= 500
70}
71
72#[cfg(test)]
73mod tests {
74 use serde_json::json;
75 use uuid::Uuid;
76 use wiremock::matchers::{body_json, method, path};
77 use wiremock::{Mock, MockServer, ResponseTemplate};
78
79 use super::*;
80 use crate::auth::TokenSource;
81
82 #[tokio::test]
83 async fn registration_retains_the_receipt_when_the_outcome_is_uncertain() {
84 let receipt = PreparedWriteReceipt {
85 base_version: Some(Uuid::now_v7()),
86 resource_id: Uuid::new_v4(),
87 sequence: 2.into(),
88 version_id: Uuid::now_v7(),
89 };
90 for status in [200, 409, 500] {
91 let server = MockServer::start().await;
92 Mock::given(method("POST"))
93 .and(path(format!(
94 "/api/tables/{}/versions",
95 receipt.resource_id
96 )))
97 .and(body_json(json!({
98 "baseVersionId": receipt.base_version,
99 "versionId": receipt.version_id,
100 })))
101 .respond_with(ResponseTemplate::new(status).set_body_json(json!({
102 "versionId": receipt.version_id,
103 "parentVersionId": receipt.base_version,
104 "seqNo": receipt.sequence,
105 })))
106 .expect(1)
107 .mount(&server)
108 .await;
109 let result = register(
110 &ArborClient::new(&server.uri())
111 .unwrap()
112 .with_token_source(TokenSource::fixed("test".into())),
113 receipt.clone(),
114 )
115 .await;
116 match status {
117 200 => assert_eq!(result.unwrap().version_id, receipt.version_id),
118 409 => assert!(matches!(result, Err(Error::Arbor(_)))),
119 _ => assert!(matches!(
120 result,
121 Err(Error::RegistrationUnknown { receipt: retained, .. }) if retained == receipt
122 )),
123 }
124 }
125
126 let unavailable = ArborClient::new("http://127.0.0.1:9")
127 .unwrap()
128 .with_token_source(TokenSource::fixed("test".into()));
129 assert!(matches!(
130 register(&unavailable, receipt.clone()).await,
131 Err(Error::RegistrationUnknown { receipt: retained, .. }) if retained == receipt
132 ));
133 }
134}