use std::future::Future;
use serde_json::Value;
use vta_sdk::client::VtaClient;
use vta_sdk::error::VtaError;
use vta_sdk::protocols::app_state::{
AppStateListResponse, AppStatePutManyResponse, AppStateWrite, PutManyMode, WriteResult,
};
use super::NAMESPACE;
const PAGE_SIZE: usize = 100;
pub(crate) const PUT_BATCH: usize = 16;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SyncError {
Unsupported,
Unreachable(String),
Refused(String),
Contract(String),
WatermarkTooOld,
}
impl SyncError {
#[must_use]
pub fn explain(&self) -> String {
match self {
SyncError::Unsupported => "This VTA does not offer application-state storage, so \
settings stay on this device only. Upgrading the VTA enables sync."
.to_string(),
SyncError::Unreachable(e) => {
format!("Could not reach the VTA to sync settings ({e}); will retry.")
}
SyncError::Refused(e) => format!(
"The VTA refused to store this account's settings ({e}). Check this \
account's access to its context."
),
SyncError::Contract(e) => format!(
"The VTA answered the settings sync in an unexpected way ({e}). This build \
and the VTA may disagree on the app-state contract."
),
SyncError::WatermarkTooOld => {
"The VTA no longer holds the change history this device needs; \
re-reading everything."
.to_string()
}
}
}
#[must_use]
pub fn is_transient(&self) -> bool {
matches!(self, SyncError::Unreachable(_) | SyncError::WatermarkTooOld)
}
}
impl From<VtaError> for SyncError {
fn from(e: VtaError) -> Self {
match e {
VtaError::UnsupportedTaskType { .. } => SyncError::Unsupported,
VtaError::Network(_)
| VtaError::DidcommTransport(_)
| VtaError::TspTransport(_)
| VtaError::Server { .. }
| VtaError::Unavailable { .. } => SyncError::Unreachable(e.to_string()),
VtaError::Auth(_) | VtaError::Forbidden(_) => SyncError::Refused(e.to_string()),
VtaError::Protocol(ref m) if m.contains("watermarkTooOld") => {
SyncError::WatermarkTooOld
}
other => SyncError::Contract(other.to_string()),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeleteOutcome {
Deleted,
Conflict,
}
pub trait AppStateStore: Send + Sync {
fn changes_since(
&self,
context_id: &str,
since: u64,
cursor: Option<&str>,
) -> impl Future<Output = Result<AppStateListResponse, SyncError>> + Send;
fn snapshot(
&self,
context_id: &str,
cursor: Option<&str>,
) -> impl Future<Output = Result<AppStateListResponse, SyncError>> + Send;
fn put_many(
&self,
context_id: &str,
writes: Vec<AppStateWrite>,
) -> impl Future<Output = Result<Vec<WriteResult>, SyncError>> + Send;
fn delete(
&self,
context_id: &str,
key: &str,
expected_version: u64,
) -> impl Future<Output = Result<DeleteOutcome, SyncError>> + Send;
}
fn parse<T: serde::de::DeserializeOwned>(v: Value) -> Result<T, SyncError> {
serde_json::from_value(v).map_err(|e| SyncError::Contract(e.to_string()))
}
impl AppStateStore for VtaClient {
async fn changes_since(
&self,
context_id: &str,
since: u64,
cursor: Option<&str>,
) -> Result<AppStateListResponse, SyncError> {
let v = self
.app_state_changes_since(
context_id,
NAMESPACE,
since,
None,
true,
Some(PAGE_SIZE),
cursor,
)
.await?;
parse(v)
}
async fn snapshot(
&self,
context_id: &str,
cursor: Option<&str>,
) -> Result<AppStateListResponse, SyncError> {
let v = self
.app_state_list(
context_id,
Some(NAMESPACE),
None,
true,
Some(PAGE_SIZE),
cursor,
)
.await?;
parse(v)
}
async fn put_many(
&self,
context_id: &str,
writes: Vec<AppStateWrite>,
) -> Result<Vec<WriteResult>, SyncError> {
let v = self
.app_state_put_many(context_id, NAMESPACE, writes, PutManyMode::Independent)
.await?;
Ok(parse::<AppStatePutManyResponse>(v)?.results)
}
async fn delete(
&self,
context_id: &str,
key: &str,
expected_version: u64,
) -> Result<DeleteOutcome, SyncError> {
match self
.app_state_delete(context_id, NAMESPACE, key, Some(expected_version))
.await
{
Ok(_) => Ok(DeleteOutcome::Deleted),
Err(VtaError::Conflict(_)) => Ok(DeleteOutcome::Conflict),
Err(VtaError::Protocol(m)) if m.contains("versionConflict") => {
Ok(DeleteOutcome::Conflict)
}
Err(e) => Err(e.into()),
}
}
}