1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11pub type QueryValue = Value;
13pub type QueryRow = Map<String, QueryValue>;
15
16#[derive(Debug, Clone)]
20pub struct WindowBase {
21 pub table: String,
22 pub variable: String,
23 pub fixed_scopes: Vec<(String, Vec<String>)>,
25 pub params: Option<String>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
35#[serde(rename_all = "camelCase")]
36pub struct WindowState {
37 pub units: Vec<String>,
39 pub pending: Vec<String>,
41}
42
43#[derive(Debug, Clone, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct TableChange {
46 pub table: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub scope_keys: Option<Vec<String>>,
49}
50
51#[derive(Debug, Clone, Serialize)]
52#[serde(rename_all = "camelCase")]
53pub struct WindowChange {
54 pub base_key: String,
55 pub table: String,
56 pub units: Vec<String>,
57}
58
59#[derive(Debug, Clone, Serialize)]
60#[serde(rename_all = "camelCase")]
61pub struct SyncStatusSnapshot {
62 pub current_schema_version: i32,
63 pub outbox: usize,
64 pub upgrading: bool,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub lease_state: Option<LeaseState>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub schema_floor: Option<SchemaFloor>,
69 pub sync_needed: bool,
70}
71
72pub const CLIENT_DIAGNOSTICS_VERSION: u8 = 1;
73pub const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS: usize = 256;
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct ExpectedDiagnosticSubscription {
78 pub id: String,
79 pub table: String,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct ClientDiagnosticsRequest {
85 #[serde(default)]
86 pub expected_subscriptions: Vec<ExpectedDiagnosticSubscription>,
87}
88
89#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
90#[serde(rename_all = "camelCase")]
91pub struct ClientDiagnosticsHost {
92 pub kind: String,
93 pub role: String,
94 pub connectivity: String,
95 pub realtime: String,
96}
97
98#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
99#[serde(rename_all = "camelCase")]
100pub struct DiagnosticSubscription {
101 pub id: String,
102 pub table: String,
103 pub state: String,
104 pub complete: bool,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub cursor: Option<i64>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 pub reason_code: Option<String>,
109}
110
111#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
112#[serde(rename_all = "camelCase")]
113pub struct DiagnosticRoundCounters {
114 pub pushed: u32,
115 pub applied: usize,
116 pub rejected: usize,
117 pub retryable: usize,
118 pub conflicts: u32,
119 pub commits_applied: u32,
120 pub segment_rows_applied: u32,
121 pub bootstrapping: usize,
122 pub resets: usize,
123 pub revoked: usize,
124 pub failed: usize,
125 pub deferred_commits: usize,
126}
127
128#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
129#[serde(rename_all = "camelCase")]
130pub struct DiagnosticLastRound {
131 pub status: String,
132 pub started_at_ms: i64,
133 pub completed_at_ms: i64,
134 pub duration_ms: i64,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub counters: Option<DiagnosticRoundCounters>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub error_code: Option<String>,
139}
140
141#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
142#[serde(rename_all = "camelCase")]
143pub struct DiagnosticLastChange {
144 pub revision: String,
145 pub recorded_at_ms: i64,
146 pub tables: Vec<String>,
147 pub windows: Vec<String>,
148 pub domains_truncated: bool,
149 pub status_changed: bool,
150 pub conflicts_changed: bool,
151 pub rejections_changed: bool,
152 pub outcomes_changed: bool,
153}
154
155#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
156#[serde(rename_all = "camelCase")]
157pub struct ClientDiagnosticsStorage {
158 pub status: String,
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub database_bytes_approx: Option<i64>,
161 #[serde(skip_serializing_if = "Option::is_none")]
162 pub pending_outbox_bytes_approx: Option<i64>,
163 #[serde(skip_serializing_if = "Option::is_none")]
164 pub retained_outcome_bytes_approx: Option<i64>,
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub retained_outcome_entries: Option<i64>,
167 #[serde(skip_serializing_if = "Option::is_none")]
168 pub blob_cache_bytes_approx: Option<i64>,
169 #[serde(skip_serializing_if = "Option::is_none")]
170 pub pressure_reason_code: Option<String>,
171}
172
173#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
174#[serde(rename_all = "camelCase")]
175pub struct ClientDiagnosticsSnapshot {
176 pub version: u8,
177 pub captured_at_ms: i64,
178 pub host: ClientDiagnosticsHost,
179 pub security_lifecycle: String,
180 pub schema: ClientDiagnosticsSchema,
181 pub replica: ClientDiagnosticsReplica,
182 pub lease: ClientDiagnosticsLease,
183 pub subscriptions: Vec<DiagnosticSubscription>,
184 pub subscriptions_truncated: bool,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub last_round: Option<DiagnosticLastRound>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub last_change: Option<DiagnosticLastChange>,
189 pub storage: ClientDiagnosticsStorage,
190}
191
192#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
193#[serde(rename_all = "camelCase")]
194pub struct ClientDiagnosticsSchema {
195 pub current_version: i32,
196 pub upgrading: bool,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub required_version: Option<i32>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub latest_version: Option<i32>,
201}
202
203#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
204#[serde(rename_all = "camelCase")]
205pub struct ClientDiagnosticsReplica {
206 pub local_revision: String,
207 pub sync_needed: bool,
208 pub pending_outbox: usize,
209}
210
211#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
212#[serde(rename_all = "camelCase")]
213pub struct ClientDiagnosticsLease {
214 pub state: String,
215 #[serde(skip_serializing_if = "Option::is_none")]
216 pub expires_at_ms: Option<i64>,
217 #[serde(skip_serializing_if = "Option::is_none")]
218 pub error_code: Option<String>,
219}
220
221#[derive(Debug, Clone, Serialize)]
223#[serde(rename_all = "camelCase")]
224pub struct ClientChangeBatch {
225 pub revision: String,
226 pub tables: Vec<TableChange>,
227 pub windows: Vec<WindowChange>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub status: Option<SyncStatusSnapshot>,
230 pub conflicts_changed: bool,
231 pub rejections_changed: bool,
232 pub outcomes_changed: bool,
233}
234
235#[derive(Debug, Clone, Serialize)]
236#[serde(tag = "kind", rename_all = "camelCase")]
237pub enum SyncIntent {
238 None,
239 Interactive,
240 Background {
241 #[serde(rename = "delayMs")]
242 delay_ms: u64,
243 },
244}
245
246#[derive(Debug, Clone, Serialize)]
247pub struct CommandEffects {
248 pub sync: SyncIntent,
249}
250
251impl CommandEffects {
252 #[must_use]
253 pub fn none() -> Self {
254 Self {
255 sync: SyncIntent::None,
256 }
257 }
258
259 #[must_use]
260 pub fn interactive() -> Self {
261 Self {
262 sync: SyncIntent::Interactive,
263 }
264 }
265}
266
267#[derive(Debug, Clone)]
268pub struct WindowCoverage {
269 pub base: WindowBase,
270 pub units: Vec<String>,
271}
272
273#[derive(Debug, Clone, Serialize)]
274#[serde(rename_all = "camelCase")]
275pub struct WindowUnitRef {
276 pub base_key: String,
277 pub unit: String,
278}
279
280#[derive(Debug, Clone, Serialize)]
281#[serde(rename_all = "camelCase")]
282pub struct CoverageSnapshot {
283 pub complete: bool,
284 pub pending: Vec<WindowUnitRef>,
285 pub missing: Vec<WindowUnitRef>,
286}
287
288#[derive(Debug, Clone, Serialize)]
289#[serde(rename_all = "camelCase")]
290pub struct QuerySnapshot {
291 pub revision: String,
292 pub rows: Vec<QueryRow>,
293 pub coverage: CoverageSnapshot,
294}
295
296impl WindowState {
297 #[must_use]
299 pub fn complete(&self, unit: &str) -> bool {
300 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
301 }
302}
303
304#[derive(Debug, Clone)]
306pub enum Mutation {
307 Upsert {
308 table: String,
309 values: Map<String, Value>,
310 base_version: Option<i64>,
311 },
312 Delete {
313 table: String,
314 row_id: String,
315 base_version: Option<i64>,
316 },
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
320#[serde(rename_all = "camelCase")]
321pub struct SchemaFloor {
322 #[serde(skip_serializing_if = "Option::is_none")]
323 pub required_schema_version: Option<i32>,
324 #[serde(skip_serializing_if = "Option::is_none")]
325 pub latest_schema_version: Option<i32>,
326}
327
328#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
332#[serde(rename_all = "camelCase")]
333pub struct LeaseState {
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub lease_id: Option<String>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub expires_at_ms: Option<i64>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub error_code: Option<String>,
340}
341
342#[derive(Debug, Clone, Serialize)]
344#[serde(rename_all = "camelCase")]
345pub struct PresencePeer {
346 pub actor_id: String,
347 pub client_id: String,
348 pub doc: serde_json::Value,
349}
350
351#[derive(Debug, Clone, Serialize, Default)]
352#[serde(rename_all = "camelCase")]
353pub struct SyncReport {
354 pub pushed: u32,
355 pub applied: Vec<String>,
356 pub rejected: Vec<String>,
357 pub retryable: Vec<String>,
358 pub conflicts: u32,
359 pub commits_applied: u32,
360 pub segment_rows_applied: u32,
361 pub bootstrapping: Vec<String>,
362 pub resets: Vec<String>,
363 pub revoked: Vec<String>,
364 pub failed: Vec<String>,
365 pub deferred_commits: usize,
366 #[serde(skip_serializing_if = "Option::is_none")]
367 pub schema_floor: Option<SchemaFloor>,
368}
369
370#[derive(Debug, Clone)]
373pub enum SyncOutcome {
374 Ok(SyncReport),
375 Failed { error_code: String, message: String },
376}
377
378impl SyncOutcome {
379 pub fn to_json(&self) -> Value {
380 match self {
381 SyncOutcome::Ok(report) => {
382 let mut map = Map::new();
383 map.insert("ok".to_owned(), Value::Bool(true));
384 map.insert(
385 "report".to_owned(),
386 serde_json::to_value(report).expect("report serializes"),
387 );
388 Value::Object(map)
389 }
390 SyncOutcome::Failed {
391 error_code,
392 message,
393 } => {
394 let mut map = Map::new();
395 map.insert("ok".to_owned(), Value::Bool(false));
396 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
397 map.insert("message".to_owned(), Value::from(message.clone()));
398 Value::Object(map)
399 }
400 }
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
405#[serde(rename_all = "camelCase")]
406pub struct ConflictRecord {
407 pub client_commit_id: String,
408 pub op_index: i32,
409 pub table: String,
410 pub row_id: String,
411 pub code: String,
412 pub message: String,
413 pub server_version: i64,
414 pub server_row: Map<String, Value>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub operation: Option<CommitOperation>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
424#[serde(rename_all = "camelCase", deny_unknown_fields)]
425pub struct RejectionDetails {
426 #[serde(skip_serializing_if = "Option::is_none")]
427 pub field_paths: Option<Vec<String>>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub reason: Option<String>,
430 #[serde(skip_serializing_if = "Option::is_none")]
431 pub required_action: Option<String>,
432 #[serde(skip_serializing_if = "Option::is_none")]
433 pub references: Option<BTreeMap<String, String>>,
434}
435
436impl RejectionDetails {
437 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
438 if raw.len() > 4_096 {
439 return Err("rejection details exceed 4096 encoded bytes".to_owned());
440 }
441 let details: Self = serde_json::from_str(raw)
442 .map_err(|error| format!("invalid rejection details: {error}"))?;
443 details.validate()?;
444 Ok(details)
445 }
446
447 fn validate(&self) -> Result<(), String> {
448 let mut members = 0;
449 if let Some(paths) = &self.field_paths {
450 members += 1;
451 if paths.is_empty() || paths.len() > 32 {
452 return Err("fieldPaths must contain 1-32 paths".to_owned());
453 }
454 let mut seen = std::collections::BTreeSet::new();
455 for path in paths {
456 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
457 {
458 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
459 }
460 }
461 }
462 if let Some(reason) = &self.reason {
463 members += 1;
464 if !valid_token(reason, 96) {
465 return Err("reason must be a lowercase stable token".to_owned());
466 }
467 }
468 if let Some(action) = &self.required_action {
469 members += 1;
470 if !valid_token(action, 96) {
471 return Err("requiredAction must be a lowercase stable token".to_owned());
472 }
473 }
474 if let Some(references) = &self.references {
475 members += 1;
476 if references.is_empty() || references.len() > 16 {
477 return Err("references must contain 1-16 entries".to_owned());
478 }
479 for (key, value) in references {
480 if !valid_token(key, 64)
481 || value.is_empty()
482 || value.len() > 256
483 || value.trim() != value
484 || value.chars().any(char::is_control)
485 {
486 return Err("references contains an invalid key or value".to_owned());
487 }
488 }
489 }
490 if members == 0 {
491 return Err("rejection details must not be empty".to_owned());
492 }
493 Ok(())
494 }
495}
496
497fn valid_identifier(segment: &str) -> bool {
498 let mut chars = segment.chars();
499 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
500 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
501}
502
503fn valid_token(token: &str, max: usize) -> bool {
504 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
505 {
506 return false;
507 }
508 let mut previous_separator = false;
509 for character in token.chars() {
510 if character.is_ascii_lowercase() || character.is_ascii_digit() {
511 previous_separator = false;
512 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
513 previous_separator = true;
514 } else {
515 return false;
516 }
517 }
518 !previous_separator
519}
520
521#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
522#[serde(rename_all = "camelCase")]
523pub struct RejectionRecord {
524 pub client_commit_id: String,
525 pub op_index: i32,
526 pub code: String,
527 pub message: String,
528 pub retryable: bool,
529 #[serde(skip_serializing_if = "Option::is_none")]
530 pub details: Option<RejectionDetails>,
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub operation: Option<CommitOperation>,
533}
534
535#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
537#[serde(rename_all = "camelCase")]
538pub struct CommitOperation {
539 pub table: String,
540 pub row_id: String,
541 pub op: String,
542 #[serde(skip_serializing_if = "Option::is_none")]
543 pub base_version: Option<i64>,
544 #[serde(skip_serializing_if = "Option::is_none")]
545 pub values: Option<Map<String, Value>>,
546 #[serde(skip_serializing_if = "Option::is_none")]
549 pub changed_fields: Option<Vec<String>>,
550}
551
552#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
553#[serde(rename_all = "snake_case")]
554pub enum CommitOutcomeStatus {
555 Applied,
556 Cached,
557 Conflict,
558 Rejected,
559}
560
561#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
562#[serde(rename_all = "snake_case")]
563pub enum CommitOutcomeResolution {
564 Active,
565 ResolvedKeepServer,
566 Superseded,
567 Dismissed,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
571#[serde(tag = "status", rename_all = "snake_case")]
572pub enum CommitOperationOutcome {
573 Applied {
574 #[serde(rename = "opIndex")]
575 op_index: i32,
576 },
577 Conflict {
578 conflict: ConflictRecord,
579 },
580 Error {
581 rejection: RejectionRecord,
582 },
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
586#[serde(rename_all = "camelCase")]
587pub struct CommitOutcome {
588 pub sequence: i64,
589 pub client_commit_id: String,
590 pub status: CommitOutcomeStatus,
591 pub recorded_at_ms: i64,
592 pub results: Vec<CommitOperationOutcome>,
593 #[serde(skip_serializing_if = "Option::is_none")]
596 pub operations: Option<Vec<CommitOperation>>,
597 pub resolution: CommitOutcomeResolution,
598 #[serde(skip_serializing_if = "Option::is_none")]
599 pub resolved_at_ms: Option<i64>,
600 #[serde(skip_serializing_if = "Option::is_none")]
601 pub replacement_client_commit_id: Option<String>,
602}
603
604#[derive(Debug, Clone, Deserialize, Default)]
605#[serde(rename_all = "camelCase")]
606pub struct CommitOutcomeQuery {
607 pub limit: Option<usize>,
608 #[serde(default)]
609 pub active_only: bool,
610}
611
612#[derive(Debug, Clone, Deserialize)]
613#[serde(rename_all = "camelCase")]
614pub struct ResolveCommitOutcomeInput {
615 pub client_commit_id: String,
616 pub resolution: CommitOutcomeResolution,
617 pub replacement_client_commit_id: Option<String>,
618}
619
620#[derive(Debug, Clone, Serialize)]
621#[serde(rename_all = "camelCase")]
622pub struct RowState {
623 pub row_id: String,
624 pub version: i64,
627 pub values: Map<String, Value>,
628}
629
630#[derive(Debug, Clone, Serialize)]
631#[serde(rename_all = "camelCase")]
632pub struct SubscriptionStateView {
633 pub id: String,
634 pub table: String,
635 pub status: String,
637 pub cursor: i64,
638 pub has_resume_token: bool,
639 #[serde(skip_serializing_if = "Option::is_none")]
640 pub effective_scopes: Option<Value>,
641 #[serde(skip_serializing_if = "Option::is_none")]
642 pub reason_code: Option<String>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
649#[serde(rename_all = "camelCase", deny_unknown_fields)]
650pub struct LocalDataPurgeTarget {
651 pub table: String,
652 pub selectors: BTreeMap<String, Vec<String>>,
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
658#[serde(rename_all = "camelCase", deny_unknown_fields)]
659pub struct LocalDataPurgeInput {
660 pub purge_id: String,
661 pub targets: Vec<LocalDataPurgeTarget>,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
666#[serde(rename_all = "camelCase")]
667pub struct LocalDataPurgeResult {
668 pub already_applied: bool,
669 pub purged_rows: usize,
670 pub dropped_commits: usize,
671}
672
673#[derive(Debug, Clone, Default)]
675pub struct ClientLimits {
676 pub limit_commits: Option<i32>,
677 pub limit_snapshot_rows: Option<i32>,
678 pub max_snapshot_pages: Option<i32>,
679 pub accept: Option<u8>,
682 pub blob_cache_max_bytes: Option<i64>,
686 pub outcome_retention_max_entries: Option<usize>,
689}