1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11#[derive(Debug, Clone)]
15pub struct WindowBase {
16 pub table: String,
17 pub variable: String,
18 pub fixed_scopes: Vec<(String, Vec<String>)>,
20 pub params: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
30#[serde(rename_all = "camelCase")]
31pub struct WindowState {
32 pub units: Vec<String>,
34 pub pending: Vec<String>,
36}
37
38#[derive(Debug, Clone, Serialize)]
39#[serde(rename_all = "camelCase")]
40pub struct TableChange {
41 pub table: String,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub scope_keys: Option<Vec<String>>,
44}
45
46#[derive(Debug, Clone, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct WindowChange {
49 pub base_key: String,
50 pub table: String,
51 pub units: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize)]
55#[serde(rename_all = "camelCase")]
56pub struct SyncStatusSnapshot {
57 pub outbox: usize,
58 pub upgrading: bool,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub lease_state: Option<LeaseState>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub schema_floor: Option<SchemaFloor>,
63 pub sync_needed: bool,
64}
65
66#[derive(Debug, Clone, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct ClientChangeBatch {
70 pub revision: String,
71 pub tables: Vec<TableChange>,
72 pub windows: Vec<WindowChange>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub status: Option<SyncStatusSnapshot>,
75 pub conflicts_changed: bool,
76 pub rejections_changed: bool,
77 pub outcomes_changed: bool,
78}
79
80#[derive(Debug, Clone, Serialize)]
81#[serde(tag = "kind", rename_all = "camelCase")]
82pub enum SyncIntent {
83 None,
84 Interactive,
85 Background {
86 #[serde(rename = "delayMs")]
87 delay_ms: u64,
88 },
89}
90
91#[derive(Debug, Clone, Serialize)]
92pub struct CommandEffects {
93 pub sync: SyncIntent,
94}
95
96impl CommandEffects {
97 #[must_use]
98 pub fn none() -> Self {
99 Self {
100 sync: SyncIntent::None,
101 }
102 }
103
104 #[must_use]
105 pub fn interactive() -> Self {
106 Self {
107 sync: SyncIntent::Interactive,
108 }
109 }
110}
111
112#[derive(Debug, Clone)]
113pub struct WindowCoverage {
114 pub base: WindowBase,
115 pub units: Vec<String>,
116}
117
118#[derive(Debug, Clone, Serialize)]
119#[serde(rename_all = "camelCase")]
120pub struct WindowUnitRef {
121 pub base_key: String,
122 pub unit: String,
123}
124
125#[derive(Debug, Clone, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct CoverageSnapshot {
128 pub complete: bool,
129 pub pending: Vec<WindowUnitRef>,
130 pub missing: Vec<WindowUnitRef>,
131}
132
133#[derive(Debug, Clone, Serialize)]
134#[serde(rename_all = "camelCase")]
135pub struct QuerySnapshot {
136 pub revision: String,
137 pub rows: Vec<Map<String, Value>>,
138 pub coverage: CoverageSnapshot,
139}
140
141impl WindowState {
142 #[must_use]
144 pub fn complete(&self, unit: &str) -> bool {
145 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
146 }
147}
148
149#[derive(Debug, Clone)]
151pub enum Mutation {
152 Upsert {
153 table: String,
154 values: Map<String, Value>,
155 base_version: Option<i64>,
156 },
157 Delete {
158 table: String,
159 row_id: String,
160 base_version: Option<i64>,
161 },
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
165#[serde(rename_all = "camelCase")]
166pub struct SchemaFloor {
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub required_schema_version: Option<i32>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub latest_schema_version: Option<i32>,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
177#[serde(rename_all = "camelCase")]
178pub struct LeaseState {
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub lease_id: Option<String>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub expires_at_ms: Option<i64>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub error_code: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize)]
189#[serde(rename_all = "camelCase")]
190pub struct PresencePeer {
191 pub actor_id: String,
192 pub client_id: String,
193 pub doc: serde_json::Value,
194}
195
196#[derive(Debug, Clone, Serialize, Default)]
197#[serde(rename_all = "camelCase")]
198pub struct SyncReport {
199 pub pushed: u32,
200 pub applied: Vec<String>,
201 pub rejected: Vec<String>,
202 pub retryable: Vec<String>,
203 pub conflicts: u32,
204 pub commits_applied: u32,
205 pub segment_rows_applied: u32,
206 pub bootstrapping: Vec<String>,
207 pub resets: Vec<String>,
208 pub revoked: Vec<String>,
209 pub failed: Vec<String>,
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub schema_floor: Option<SchemaFloor>,
212}
213
214#[derive(Debug, Clone)]
217pub enum SyncOutcome {
218 Ok(SyncReport),
219 Failed { error_code: String, message: String },
220}
221
222impl SyncOutcome {
223 pub fn to_json(&self) -> Value {
224 match self {
225 SyncOutcome::Ok(report) => {
226 let mut map = Map::new();
227 map.insert("ok".to_owned(), Value::Bool(true));
228 map.insert(
229 "report".to_owned(),
230 serde_json::to_value(report).expect("report serializes"),
231 );
232 Value::Object(map)
233 }
234 SyncOutcome::Failed {
235 error_code,
236 message,
237 } => {
238 let mut map = Map::new();
239 map.insert("ok".to_owned(), Value::Bool(false));
240 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
241 map.insert("message".to_owned(), Value::from(message.clone()));
242 Value::Object(map)
243 }
244 }
245 }
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
249#[serde(rename_all = "camelCase")]
250pub struct ConflictRecord {
251 pub client_commit_id: String,
252 pub op_index: i32,
253 pub table: String,
254 pub row_id: String,
255 pub code: String,
256 pub message: String,
257 pub server_version: i64,
258 pub server_row: Map<String, Value>,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub operation: Option<CommitOperation>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
268#[serde(rename_all = "camelCase", deny_unknown_fields)]
269pub struct RejectionDetails {
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub field_paths: Option<Vec<String>>,
272 #[serde(skip_serializing_if = "Option::is_none")]
273 pub reason: Option<String>,
274 #[serde(skip_serializing_if = "Option::is_none")]
275 pub required_action: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub references: Option<BTreeMap<String, String>>,
278}
279
280impl RejectionDetails {
281 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
282 if raw.len() > 4_096 {
283 return Err("rejection details exceed 4096 encoded bytes".to_owned());
284 }
285 let details: Self = serde_json::from_str(raw)
286 .map_err(|error| format!("invalid rejection details: {error}"))?;
287 details.validate()?;
288 Ok(details)
289 }
290
291 fn validate(&self) -> Result<(), String> {
292 let mut members = 0;
293 if let Some(paths) = &self.field_paths {
294 members += 1;
295 if paths.is_empty() || paths.len() > 32 {
296 return Err("fieldPaths must contain 1-32 paths".to_owned());
297 }
298 let mut seen = std::collections::BTreeSet::new();
299 for path in paths {
300 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
301 {
302 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
303 }
304 }
305 }
306 if let Some(reason) = &self.reason {
307 members += 1;
308 if !valid_token(reason, 96) {
309 return Err("reason must be a lowercase stable token".to_owned());
310 }
311 }
312 if let Some(action) = &self.required_action {
313 members += 1;
314 if !valid_token(action, 96) {
315 return Err("requiredAction must be a lowercase stable token".to_owned());
316 }
317 }
318 if let Some(references) = &self.references {
319 members += 1;
320 if references.is_empty() || references.len() > 16 {
321 return Err("references must contain 1-16 entries".to_owned());
322 }
323 for (key, value) in references {
324 if !valid_token(key, 64)
325 || value.is_empty()
326 || value.len() > 256
327 || value.trim() != value
328 || value.chars().any(char::is_control)
329 {
330 return Err("references contains an invalid key or value".to_owned());
331 }
332 }
333 }
334 if members == 0 {
335 return Err("rejection details must not be empty".to_owned());
336 }
337 Ok(())
338 }
339}
340
341fn valid_identifier(segment: &str) -> bool {
342 let mut chars = segment.chars();
343 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
344 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
345}
346
347fn valid_token(token: &str, max: usize) -> bool {
348 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
349 {
350 return false;
351 }
352 let mut previous_separator = false;
353 for character in token.chars() {
354 if character.is_ascii_lowercase() || character.is_ascii_digit() {
355 previous_separator = false;
356 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
357 previous_separator = true;
358 } else {
359 return false;
360 }
361 }
362 !previous_separator
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
366#[serde(rename_all = "camelCase")]
367pub struct RejectionRecord {
368 pub client_commit_id: String,
369 pub op_index: i32,
370 pub code: String,
371 pub message: String,
372 pub retryable: bool,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 pub details: Option<RejectionDetails>,
375 #[serde(skip_serializing_if = "Option::is_none")]
376 pub operation: Option<CommitOperation>,
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
381#[serde(rename_all = "camelCase")]
382pub struct CommitOperation {
383 pub table: String,
384 pub row_id: String,
385 pub op: String,
386 #[serde(skip_serializing_if = "Option::is_none")]
387 pub base_version: Option<i64>,
388 #[serde(skip_serializing_if = "Option::is_none")]
389 pub values: Option<Map<String, Value>>,
390 #[serde(skip_serializing_if = "Option::is_none")]
393 pub changed_fields: Option<Vec<String>>,
394}
395
396#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
397#[serde(rename_all = "snake_case")]
398pub enum CommitOutcomeStatus {
399 Applied,
400 Cached,
401 Conflict,
402 Rejected,
403}
404
405#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
406#[serde(rename_all = "snake_case")]
407pub enum CommitOutcomeResolution {
408 Active,
409 ResolvedKeepServer,
410 Superseded,
411 Dismissed,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
415#[serde(tag = "status", rename_all = "snake_case")]
416pub enum CommitOperationOutcome {
417 Applied {
418 #[serde(rename = "opIndex")]
419 op_index: i32,
420 },
421 Conflict {
422 conflict: ConflictRecord,
423 },
424 Error {
425 rejection: RejectionRecord,
426 },
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
430#[serde(rename_all = "camelCase")]
431pub struct CommitOutcome {
432 pub sequence: i64,
433 pub client_commit_id: String,
434 pub status: CommitOutcomeStatus,
435 pub recorded_at_ms: i64,
436 pub results: Vec<CommitOperationOutcome>,
437 #[serde(skip_serializing_if = "Option::is_none")]
440 pub operations: Option<Vec<CommitOperation>>,
441 pub resolution: CommitOutcomeResolution,
442 #[serde(skip_serializing_if = "Option::is_none")]
443 pub resolved_at_ms: Option<i64>,
444 #[serde(skip_serializing_if = "Option::is_none")]
445 pub replacement_client_commit_id: Option<String>,
446}
447
448#[derive(Debug, Clone, Deserialize, Default)]
449#[serde(rename_all = "camelCase")]
450pub struct CommitOutcomeQuery {
451 pub limit: Option<usize>,
452 #[serde(default)]
453 pub active_only: bool,
454}
455
456#[derive(Debug, Clone, Deserialize)]
457#[serde(rename_all = "camelCase")]
458pub struct ResolveCommitOutcomeInput {
459 pub client_commit_id: String,
460 pub resolution: CommitOutcomeResolution,
461 pub replacement_client_commit_id: Option<String>,
462}
463
464#[derive(Debug, Clone, Serialize)]
465#[serde(rename_all = "camelCase")]
466pub struct RowState {
467 pub row_id: String,
468 pub version: i64,
471 pub values: Map<String, Value>,
472}
473
474#[derive(Debug, Clone, Serialize)]
475#[serde(rename_all = "camelCase")]
476pub struct SubscriptionStateView {
477 pub id: String,
478 pub table: String,
479 pub status: String,
481 pub cursor: i64,
482 pub has_resume_token: bool,
483 #[serde(skip_serializing_if = "Option::is_none")]
484 pub effective_scopes: Option<Value>,
485 #[serde(skip_serializing_if = "Option::is_none")]
486 pub reason_code: Option<String>,
487}
488
489#[derive(Debug, Clone, Default)]
491pub struct ClientLimits {
492 pub limit_commits: Option<i32>,
493 pub limit_snapshot_rows: Option<i32>,
494 pub max_snapshot_pages: Option<i32>,
495 pub accept: Option<u8>,
498 pub blob_cache_max_bytes: Option<i64>,
502 pub outcome_retention_max_entries: Option<usize>,
505}