use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
#[derive(Debug, Clone)]
pub struct WindowBase {
pub table: String,
pub variable: String,
pub fixed_scopes: Vec<(String, Vec<String>)>,
pub params: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WindowState {
pub units: Vec<String>,
pub pending: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TableChange {
pub table: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope_keys: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowChange {
pub base_key: String,
pub table: String,
pub units: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SyncStatusSnapshot {
pub outbox: usize,
pub upgrading: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub lease_state: Option<LeaseState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_floor: Option<SchemaFloor>,
pub sync_needed: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientChangeBatch {
pub revision: String,
pub tables: Vec<TableChange>,
pub windows: Vec<WindowChange>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<SyncStatusSnapshot>,
pub conflicts_changed: bool,
pub rejections_changed: bool,
pub outcomes_changed: bool,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
pub enum SyncIntent {
None,
Interactive,
Background {
#[serde(rename = "delayMs")]
delay_ms: u64,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct CommandEffects {
pub sync: SyncIntent,
}
impl CommandEffects {
#[must_use]
pub fn none() -> Self {
Self {
sync: SyncIntent::None,
}
}
#[must_use]
pub fn interactive() -> Self {
Self {
sync: SyncIntent::Interactive,
}
}
}
#[derive(Debug, Clone)]
pub struct WindowCoverage {
pub base: WindowBase,
pub units: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WindowUnitRef {
pub base_key: String,
pub unit: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CoverageSnapshot {
pub complete: bool,
pub pending: Vec<WindowUnitRef>,
pub missing: Vec<WindowUnitRef>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct QuerySnapshot {
pub revision: String,
pub rows: Vec<Map<String, Value>>,
pub coverage: CoverageSnapshot,
}
impl WindowState {
#[must_use]
pub fn complete(&self, unit: &str) -> bool {
self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
}
}
#[derive(Debug, Clone)]
pub enum Mutation {
Upsert {
table: String,
values: Map<String, Value>,
base_version: Option<i64>,
},
Delete {
table: String,
row_id: String,
base_version: Option<i64>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct SchemaFloor {
#[serde(skip_serializing_if = "Option::is_none")]
pub required_schema_version: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latest_schema_version: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct LeaseState {
#[serde(skip_serializing_if = "Option::is_none")]
pub lease_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PresencePeer {
pub actor_id: String,
pub client_id: String,
pub doc: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct SyncReport {
pub pushed: u32,
pub applied: Vec<String>,
pub rejected: Vec<String>,
pub retryable: Vec<String>,
pub conflicts: u32,
pub commits_applied: u32,
pub segment_rows_applied: u32,
pub bootstrapping: Vec<String>,
pub resets: Vec<String>,
pub revoked: Vec<String>,
pub failed: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schema_floor: Option<SchemaFloor>,
}
#[derive(Debug, Clone)]
pub enum SyncOutcome {
Ok(SyncReport),
Failed { error_code: String, message: String },
}
impl SyncOutcome {
pub fn to_json(&self) -> Value {
match self {
SyncOutcome::Ok(report) => {
let mut map = Map::new();
map.insert("ok".to_owned(), Value::Bool(true));
map.insert(
"report".to_owned(),
serde_json::to_value(report).expect("report serializes"),
);
Value::Object(map)
}
SyncOutcome::Failed {
error_code,
message,
} => {
let mut map = Map::new();
map.insert("ok".to_owned(), Value::Bool(false));
map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
map.insert("message".to_owned(), Value::from(message.clone()));
Value::Object(map)
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ConflictRecord {
pub client_commit_id: String,
pub op_index: i32,
pub table: String,
pub row_id: String,
pub code: String,
pub message: String,
pub server_version: i64,
pub server_row: Map<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<CommitOperation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct RejectionDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub field_paths: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required_action: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub references: Option<BTreeMap<String, String>>,
}
impl RejectionDetails {
pub(crate) fn parse(raw: &str) -> Result<Self, String> {
if raw.len() > 4_096 {
return Err("rejection details exceed 4096 encoded bytes".to_owned());
}
let details: Self = serde_json::from_str(raw)
.map_err(|error| format!("invalid rejection details: {error}"))?;
details.validate()?;
Ok(details)
}
fn validate(&self) -> Result<(), String> {
let mut members = 0;
if let Some(paths) = &self.field_paths {
members += 1;
if paths.is_empty() || paths.len() > 32 {
return Err("fieldPaths must contain 1-32 paths".to_owned());
}
let mut seen = std::collections::BTreeSet::new();
for path in paths {
if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
{
return Err("fieldPaths contains an invalid or duplicate path".to_owned());
}
}
}
if let Some(reason) = &self.reason {
members += 1;
if !valid_token(reason, 96) {
return Err("reason must be a lowercase stable token".to_owned());
}
}
if let Some(action) = &self.required_action {
members += 1;
if !valid_token(action, 96) {
return Err("requiredAction must be a lowercase stable token".to_owned());
}
}
if let Some(references) = &self.references {
members += 1;
if references.is_empty() || references.len() > 16 {
return Err("references must contain 1-16 entries".to_owned());
}
for (key, value) in references {
if !valid_token(key, 64)
|| value.is_empty()
|| value.len() > 256
|| value.trim() != value
|| value.chars().any(char::is_control)
{
return Err("references contains an invalid key or value".to_owned());
}
}
}
if members == 0 {
return Err("rejection details must not be empty".to_owned());
}
Ok(())
}
}
fn valid_identifier(segment: &str) -> bool {
let mut chars = segment.chars();
matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
&& chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
}
fn valid_token(token: &str, max: usize) -> bool {
if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
{
return false;
}
let mut previous_separator = false;
for character in token.chars() {
if character.is_ascii_lowercase() || character.is_ascii_digit() {
previous_separator = false;
} else if matches!(character, '.' | '_' | '-') && !previous_separator {
previous_separator = true;
} else {
return false;
}
}
!previous_separator
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RejectionRecord {
pub client_commit_id: String,
pub op_index: i32,
pub code: String,
pub message: String,
pub retryable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<RejectionDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<CommitOperation>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CommitOperation {
pub table: String,
pub row_id: String,
pub op: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub base_version: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<Map<String, Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub changed_fields: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CommitOutcomeStatus {
Applied,
Cached,
Conflict,
Rejected,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CommitOutcomeResolution {
Active,
ResolvedKeepServer,
Superseded,
Dismissed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum CommitOperationOutcome {
Applied {
#[serde(rename = "opIndex")]
op_index: i32,
},
Conflict {
conflict: ConflictRecord,
},
Error {
rejection: RejectionRecord,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CommitOutcome {
pub sequence: i64,
pub client_commit_id: String,
pub status: CommitOutcomeStatus,
pub recorded_at_ms: i64,
pub results: Vec<CommitOperationOutcome>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operations: Option<Vec<CommitOperation>>,
pub resolution: CommitOutcomeResolution,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolved_at_ms: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub replacement_client_commit_id: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CommitOutcomeQuery {
pub limit: Option<usize>,
#[serde(default)]
pub active_only: bool,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResolveCommitOutcomeInput {
pub client_commit_id: String,
pub resolution: CommitOutcomeResolution,
pub replacement_client_commit_id: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RowState {
pub row_id: String,
pub version: i64,
pub values: Map<String, Value>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscriptionStateView {
pub id: String,
pub table: String,
pub status: String,
pub cursor: i64,
pub has_resume_token: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub effective_scopes: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason_code: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ClientLimits {
pub limit_commits: Option<i32>,
pub limit_snapshot_rows: Option<i32>,
pub max_snapshot_pages: Option<i32>,
pub accept: Option<u8>,
pub blob_cache_max_bytes: Option<i64>,
pub outcome_retention_max_entries: Option<usize>,
}