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/// One local mutation (§6.1 shapes, schema-agnostic local form per §0).
22#[derive(Debug, Clone)]
23pub enum Mutation {
24    Upsert {
25        table: String,
26        values: Map<String, Value>,
27        base_version: Option<i64>,
28    },
29    Delete {
30        table: String,
31        row_id: String,
32        base_version: Option<i64>,
33    },
34}
35
36#[derive(Debug, Clone, Serialize, Default)]
37#[serde(rename_all = "camelCase")]
38pub struct SchemaFloor {
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub required_schema_version: Option<i32>,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub latest_schema_version: Option<i32>,
43}
44
45/// §7.3.5: the client's opaque auth-lease state — `leaseId`/`expiresAtMs`
46/// from the last `LEASE` frame, and `errorCode` once a round was rejected
47/// with a request-level lease code (stop-and-surface; no data purge).
48#[derive(Debug, Clone, Serialize, Default)]
49#[serde(rename_all = "camelCase")]
50pub struct LeaseState {
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub lease_id: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub expires_at_ms: Option<i64>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub error_code: Option<String>,
57}
58
59/// §8.6 a peer's ephemeral presence document on a scope key.
60#[derive(Debug, Clone, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct PresencePeer {
63    pub actor_id: String,
64    pub client_id: String,
65    pub doc: serde_json::Value,
66}
67
68#[derive(Debug, Clone, Serialize, Default)]
69#[serde(rename_all = "camelCase")]
70pub struct SyncReport {
71    pub pushed: u32,
72    pub applied: Vec<String>,
73    pub rejected: Vec<String>,
74    pub retryable: Vec<String>,
75    pub conflicts: u32,
76    pub commits_applied: u32,
77    pub segment_rows_applied: u32,
78    pub bootstrapping: Vec<String>,
79    pub resets: Vec<String>,
80    pub revoked: Vec<String>,
81    pub failed: Vec<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub schema_floor: Option<SchemaFloor>,
84}
85
86/// `sync()` never panics or errors out-of-band: transport and protocol
87/// failures come back as `Failed` (the driver's `{ ok: false }`).
88#[derive(Debug, Clone)]
89pub enum SyncOutcome {
90    Ok(SyncReport),
91    Failed { error_code: String, message: String },
92}
93
94impl SyncOutcome {
95    pub fn to_json(&self) -> Value {
96        match self {
97            SyncOutcome::Ok(report) => {
98                let mut map = Map::new();
99                map.insert("ok".to_owned(), Value::Bool(true));
100                map.insert(
101                    "report".to_owned(),
102                    serde_json::to_value(report).expect("report serializes"),
103                );
104                Value::Object(map)
105            }
106            SyncOutcome::Failed {
107                error_code,
108                message,
109            } => {
110                let mut map = Map::new();
111                map.insert("ok".to_owned(), Value::Bool(false));
112                map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
113                map.insert("message".to_owned(), Value::from(message.clone()));
114                Value::Object(map)
115            }
116        }
117    }
118}
119
120#[derive(Debug, Clone, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct ConflictRecord {
123    pub client_commit_id: String,
124    pub op_index: i32,
125    pub table: String,
126    pub row_id: String,
127    pub code: String,
128    pub server_version: i64,
129    /// Driver row (bytes as `{"$bytes": hex}`), decoded from the conflict
130    /// record's `serverRow` (§6.3).
131    pub server_row: Map<String, Value>,
132}
133
134#[derive(Debug, Clone, Serialize)]
135#[serde(rename_all = "camelCase")]
136pub struct RejectionRecord {
137    pub client_commit_id: String,
138    pub op_index: i32,
139    pub code: String,
140    pub retryable: bool,
141}
142
143#[derive(Debug, Clone, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct RowState {
146    pub row_id: String,
147    /// Local synced version: `-1` = optimistic, else the server version
148    /// (from a `COMMIT` change or a segment row record, §5.2/§5.6).
149    pub version: i64,
150    pub values: Map<String, Value>,
151}
152
153#[derive(Debug, Clone, Serialize)]
154#[serde(rename_all = "camelCase")]
155pub struct SubscriptionStateView {
156    pub id: String,
157    pub table: String,
158    /// `active` | `revoked` | `failed`.
159    pub status: String,
160    pub cursor: i64,
161    pub has_resume_token: bool,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub effective_scopes: Option<Value>,
164    #[serde(skip_serializing_if = "Option::is_none")]
165    pub reason_code: Option<String>,
166}
167
168/// Client limits (§4.2 request knobs).
169#[derive(Debug, Clone, Default)]
170pub struct ClientLimits {
171    pub limit_commits: Option<i32>,
172    pub limit_snapshot_rows: Option<i32>,
173    pub max_snapshot_pages: Option<i32>,
174    /// §4.2 accept bitmask; this client defaults to `0b0111` (rows
175    /// baseline + sqlite images, §5.3 — rusqlite can always import).
176    pub accept: Option<u8>,
177    /// §5.9.7 B1 blob-cache size cap (bytes). When set and the sum of cached
178    /// body sizes exceeds it, zero-ref, non-pinned bodies are evicted LRU-first
179    /// after each cache write. `None` ⇒ retain until storage pressure (default).
180    pub blob_cache_max_bytes: Option<i64>,
181}