Skip to main content

syncular_client/
api.rs

1//! Driver-facing API shapes (JSON-able), mirroring the conformance
2//! `ClientInstance` contract: sync reports, conflicts, rejections, row
3//! states, subscription states. Serialized as camelCase to cross the shim
4//! boundary unchanged.
5
6use serde::Serialize;
7use serde_json::{Map, Value};
8
9/// §4.8 window base: one table, the scope variable whose values are the
10/// window units, any fixed scopes shared by every unit, and host-opaque
11/// `params` carried onto each unit's subscription.
12#[derive(Debug, Clone)]
13pub struct WindowBase {
14    pub table: String,
15    pub variable: String,
16    /// Scopes shared by every unit (other variables), if any.
17    pub fixed_scopes: Vec<(String, Vec<String>)>,
18    pub params: Option<String>,
19}
20
21/// §4.8 completeness oracle (I3): the windowed-in units for a base, plus
22/// the subset whose bootstrap has not yet completed. Registration alone is
23/// not completeness — a `pending` unit's local replica may be empty or
24/// partial (its subscription still has `cursor: -1` or holds a resume
25/// token), and MUST NOT be rendered as complete. A unit with zero server
26/// rows still completes once its bootstrap round finishes.
27#[derive(Debug, Clone, Serialize, Default)]
28#[serde(rename_all = "camelCase")]
29pub struct WindowState {
30    /// Windowed-in units for this base, ordered by value.
31    pub units: Vec<String>,
32    /// Registered units whose bootstrap has not yet completed.
33    pub pending: Vec<String>,
34}
35
36impl WindowState {
37    /// The per-unit verdict: registered AND bootstrap-complete.
38    #[must_use]
39    pub fn complete(&self, unit: &str) -> bool {
40        self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
41    }
42}
43
44/// One local mutation (§6.1 shapes, schema-agnostic local form per §0).
45#[derive(Debug, Clone)]
46pub enum Mutation {
47    Upsert {
48        table: String,
49        values: Map<String, Value>,
50        base_version: Option<i64>,
51    },
52    Delete {
53        table: String,
54        row_id: String,
55        base_version: Option<i64>,
56    },
57}
58
59#[derive(Debug, Clone, Serialize, Default)]
60#[serde(rename_all = "camelCase")]
61pub struct SchemaFloor {
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub required_schema_version: Option<i32>,
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub latest_schema_version: Option<i32>,
66}
67
68/// §7.3.5: the client's opaque auth-lease state — `leaseId`/`expiresAtMs`
69/// from the last `LEASE` frame, and `errorCode` once a round was rejected
70/// with a request-level lease code (stop-and-surface; no data purge).
71#[derive(Debug, Clone, Serialize, Default)]
72#[serde(rename_all = "camelCase")]
73pub struct LeaseState {
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub lease_id: Option<String>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub expires_at_ms: Option<i64>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub error_code: Option<String>,
80}
81
82/// §8.6 a peer's ephemeral presence document on a scope key.
83#[derive(Debug, Clone, Serialize)]
84#[serde(rename_all = "camelCase")]
85pub struct PresencePeer {
86    pub actor_id: String,
87    pub client_id: String,
88    pub doc: serde_json::Value,
89}
90
91#[derive(Debug, Clone, Serialize, Default)]
92#[serde(rename_all = "camelCase")]
93pub struct SyncReport {
94    pub pushed: u32,
95    pub applied: Vec<String>,
96    pub rejected: Vec<String>,
97    pub retryable: Vec<String>,
98    pub conflicts: u32,
99    pub commits_applied: u32,
100    pub segment_rows_applied: u32,
101    pub bootstrapping: Vec<String>,
102    pub resets: Vec<String>,
103    pub revoked: Vec<String>,
104    pub failed: Vec<String>,
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub schema_floor: Option<SchemaFloor>,
107}
108
109/// `sync()` never panics or errors out-of-band: transport and protocol
110/// failures come back as `Failed` (the driver's `{ ok: false }`).
111#[derive(Debug, Clone)]
112pub enum SyncOutcome {
113    Ok(SyncReport),
114    Failed { error_code: String, message: String },
115}
116
117impl SyncOutcome {
118    pub fn to_json(&self) -> Value {
119        match self {
120            SyncOutcome::Ok(report) => {
121                let mut map = Map::new();
122                map.insert("ok".to_owned(), Value::Bool(true));
123                map.insert(
124                    "report".to_owned(),
125                    serde_json::to_value(report).expect("report serializes"),
126                );
127                Value::Object(map)
128            }
129            SyncOutcome::Failed {
130                error_code,
131                message,
132            } => {
133                let mut map = Map::new();
134                map.insert("ok".to_owned(), Value::Bool(false));
135                map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
136                map.insert("message".to_owned(), Value::from(message.clone()));
137                Value::Object(map)
138            }
139        }
140    }
141}
142
143#[derive(Debug, Clone, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct ConflictRecord {
146    pub client_commit_id: String,
147    pub op_index: i32,
148    pub table: String,
149    pub row_id: String,
150    pub code: String,
151    pub server_version: i64,
152    /// Driver row (bytes as `{"$bytes": hex}`), decoded from the conflict
153    /// record's `serverRow` (§6.3).
154    pub server_row: Map<String, Value>,
155}
156
157#[derive(Debug, Clone, Serialize)]
158#[serde(rename_all = "camelCase")]
159pub struct RejectionRecord {
160    pub client_commit_id: String,
161    pub op_index: i32,
162    pub code: String,
163    pub retryable: bool,
164}
165
166#[derive(Debug, Clone, Serialize)]
167#[serde(rename_all = "camelCase")]
168pub struct RowState {
169    pub row_id: String,
170    /// Local synced version: `-1` = optimistic, else the server version
171    /// (from a `COMMIT` change or a segment row record, §5.2/§5.6).
172    pub version: i64,
173    pub values: Map<String, Value>,
174}
175
176#[derive(Debug, Clone, Serialize)]
177#[serde(rename_all = "camelCase")]
178pub struct SubscriptionStateView {
179    pub id: String,
180    pub table: String,
181    /// `active` | `revoked` | `failed`.
182    pub status: String,
183    pub cursor: i64,
184    pub has_resume_token: bool,
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub effective_scopes: Option<Value>,
187    #[serde(skip_serializing_if = "Option::is_none")]
188    pub reason_code: Option<String>,
189}
190
191/// Client limits (§4.2 request knobs).
192#[derive(Debug, Clone, Default)]
193pub struct ClientLimits {
194    pub limit_commits: Option<i32>,
195    pub limit_snapshot_rows: Option<i32>,
196    pub max_snapshot_pages: Option<i32>,
197    /// §4.2 accept bitmask; this client defaults to `0b0111` (rows
198    /// baseline + sqlite images, §5.3 — rusqlite can always import).
199    pub accept: Option<u8>,
200    /// §5.9.7 B1 blob-cache size cap (bytes). When set and the sum of cached
201    /// body sizes exceeds it, zero-ref, non-pinned bodies are evicted LRU-first
202    /// after each cache write. `None` ⇒ retain until storage pressure (default).
203    pub blob_cache_max_bytes: Option<i64>,
204}