1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10
11use crate::previous_version::PreviousVersionContextConfig;
12pub type QueryValue = Value;
14pub type QueryRow = Map<String, QueryValue>;
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct FetchedBlob {
20 pub blob_id: String,
21 pub byte_length: i64,
22 pub bytes: Vec<u8>,
23 pub media_type: Option<String>,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum TimeBucketUnit {
28 Month,
29}
30
31const MAX_TIME_BUCKET_MS: i64 = 253_402_300_799_999;
32
33fn utc_year_month(timestamp_ms: i64) -> Result<(i64, i64), String> {
34 if !(0..=MAX_TIME_BUCKET_MS).contains(×tamp_ms) {
35 return Err(
36 "sync.invalid_request: time bucket timestamps must fall from 1970 through 9999"
37 .to_owned(),
38 );
39 }
40 let z = timestamp_ms / 86_400_000 + 719_468;
41 let era = z.div_euclid(146_097);
42 let day_of_era = z - era * 146_097;
43 let year_of_era =
44 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
45 let mut year = year_of_era + era * 400;
46 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
47 let month_prime = (5 * day_of_year + 2) / 153;
48 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
49 if month <= 2 {
50 year += 1;
51 }
52 Ok((year, month))
53}
54
55pub fn creation_time_bucket(created_at_ms: i64, unit: TimeBucketUnit) -> Result<String, String> {
57 match unit {
58 TimeBucketUnit::Month => {
59 let (year, month) = utc_year_month(created_at_ms)?;
60 Ok(format!("{year:04}-{month:02}"))
61 }
62 }
63}
64
65pub fn last(count: usize, unit: TimeBucketUnit, now_ms: i64) -> Result<Vec<String>, String> {
67 if !(1..=1_200).contains(&count) {
68 return Err(
69 "sync.invalid_request: time bucket count must be from 1 through 1200".to_owned(),
70 );
71 }
72 match unit {
73 TimeBucketUnit::Month => {
74 let (year, month) = utc_year_month(now_ms)?;
75 let current = year * 12 + month - 1;
76 if current - (count as i64 - 1) < 1970 * 12 {
77 return Err(
78 "sync.invalid_request: every returned UTC month must fall from 1970 through 9999"
79 .to_owned(),
80 );
81 }
82 Ok((0..count)
83 .rev()
84 .map(|offset| {
85 let value = current - offset as i64;
86 format!(
87 "{:04}-{:02}",
88 value.div_euclid(12),
89 value.rem_euclid(12) + 1
90 )
91 })
92 .collect())
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
100pub struct SubscriptionRequest {
101 pub id: String,
102 pub table: String,
103 pub scopes: Vec<(String, Vec<String>)>,
104 pub params: Option<String>,
105}
106
107#[derive(Debug, Clone)]
111pub struct WindowBase {
112 pub table: String,
113 pub variable: String,
114 pub fixed_scopes: Vec<(String, Vec<String>)>,
116 pub params: Option<String>,
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
126#[serde(rename_all = "camelCase")]
127pub struct WindowState {
128 pub units: Vec<String>,
130 pub pending: Vec<String>,
132}
133
134#[cfg(test)]
135mod time_bucket_tests {
136 use super::{creation_time_bucket, last, TimeBucketUnit};
137
138 #[test]
139 fn utc_month_buckets_cross_year_boundaries() {
140 let now = 1_770_508_800_000; assert_eq!(
142 creation_time_bucket(now, TimeBucketUnit::Month).unwrap(),
143 "2026-02"
144 );
145 assert_eq!(
146 last(3, TimeBucketUnit::Month, now).unwrap(),
147 vec!["2025-12", "2026-01", "2026-02"]
148 );
149 }
150
151 #[test]
152 fn utc_month_buckets_reject_unbounded_input() {
153 assert!(last(0, TimeBucketUnit::Month, 0).is_err());
154 assert!(last(1_201, TimeBucketUnit::Month, 0).is_err());
155 assert!(last(2, TimeBucketUnit::Month, 0).is_err());
156 assert!(creation_time_bucket(-1, TimeBucketUnit::Month).is_err());
157 }
158}
159
160#[derive(Debug, Clone, Serialize)]
161#[serde(rename_all = "camelCase")]
162pub struct TableChange {
163 pub table: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub scope_keys: Option<Vec<String>>,
166}
167
168#[derive(Debug, Clone, Serialize)]
169#[serde(rename_all = "camelCase")]
170pub struct WindowChange {
171 pub base_key: String,
172 pub table: String,
173 pub units: Vec<String>,
174}
175
176#[derive(Debug, Clone, Serialize)]
177#[serde(rename_all = "camelCase")]
178pub struct SyncStatusSnapshot {
179 pub current_schema_version: i32,
180 pub outbox: usize,
181 pub upgrading: bool,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub lease_state: Option<LeaseState>,
184 #[serde(skip_serializing_if = "Option::is_none")]
185 pub schema_floor: Option<SchemaFloor>,
186 pub sync_needed: bool,
187 pub previous_version_context: PreviousVersionStatus,
190}
191
192#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
194#[serde(rename_all = "camelCase")]
195pub struct PreviousVersionStatus {
196 pub present: bool,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub created_at_ms: Option<i64>,
199}
200
201pub const CLIENT_DIAGNOSTICS_VERSION: u8 = 1;
202pub const MAX_DIAGNOSTIC_EXPECTED_SUBSCRIPTIONS: usize = 256;
203pub const MAX_DIAGNOSTIC_QUERY_FAILURES: usize = 256;
204
205pub const REALTIME_UNAVAILABLE_CODE: &str = "sync.realtime_unavailable";
208pub const REALTIME_LOST_CODE: &str = "client.realtime_lost";
211pub const LOCAL_AUTHORITY_UNCONFIGURED_CODE: &str = "client.local_authority_unconfigured";
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
217#[serde(rename_all = "camelCase", deny_unknown_fields)]
218pub struct ExpectedDiagnosticSubscription {
219 pub id: String,
220 pub table: String,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
224#[serde(rename_all = "camelCase", deny_unknown_fields)]
225pub struct ClientDiagnosticsRequest {
226 #[serde(default)]
227 pub expected_subscriptions: Vec<ExpectedDiagnosticSubscription>,
228}
229
230#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
231#[serde(rename_all = "camelCase")]
232pub struct ClientDiagnosticsHost {
233 pub kind: String,
234 pub role: String,
235 pub connectivity: String,
236 pub realtime: RealtimeState,
237 pub realtime_policy: RealtimePolicy,
238 #[serde(skip_serializing_if = "Option::is_none")]
239 pub realtime_reason_code: Option<String>,
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub realtime_retry_delay_ms: Option<u64>,
242}
243
244#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
245#[serde(rename_all = "camelCase")]
246pub struct DiagnosticSubscription {
247 pub id: String,
248 pub table: String,
249 pub state: String,
250 pub complete: bool,
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub cursor: Option<i64>,
253 #[serde(skip_serializing_if = "Option::is_none")]
254 pub reason_code: Option<String>,
255}
256
257#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
258#[serde(rename_all = "camelCase")]
259pub struct DiagnosticRoundCounters {
260 pub pushed: u32,
261 pub applied: usize,
262 pub rejected: usize,
263 pub retryable: usize,
264 pub conflicts: u32,
265 pub commits_applied: u32,
266 pub segment_rows_applied: u32,
267 pub bootstrapping: usize,
268 pub resets: usize,
269 pub revoked: usize,
270 pub failed: usize,
271 pub deferred_commits: usize,
272}
273
274#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
275#[serde(rename_all = "camelCase")]
276pub struct DiagnosticLastRound {
277 pub status: String,
278 pub started_at_ms: i64,
279 pub completed_at_ms: i64,
280 pub duration_ms: i64,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub counters: Option<DiagnosticRoundCounters>,
283 #[serde(skip_serializing_if = "Option::is_none")]
284 pub error_code: Option<String>,
285}
286
287#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
288#[serde(rename_all = "camelCase")]
289pub struct DiagnosticLastChange {
290 pub revision: String,
291 pub recorded_at_ms: i64,
292 pub tables: Vec<String>,
293 pub windows: Vec<String>,
294 pub domains_truncated: bool,
295 pub status_changed: bool,
296 pub conflicts_changed: bool,
297 pub rejections_changed: bool,
298 pub outcomes_changed: bool,
299}
300
301#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
302#[serde(rename_all = "camelCase")]
303pub struct ClientDiagnosticsStorage {
304 pub status: String,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub database_bytes_approx: Option<i64>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub pending_outbox_bytes_approx: Option<i64>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub retained_outcome_bytes_approx: Option<i64>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub retained_outcome_entries: Option<i64>,
313 #[serde(skip_serializing_if = "Option::is_none")]
314 pub blob_cache_bytes_approx: Option<i64>,
315 #[serde(skip_serializing_if = "Option::is_none")]
316 pub pressure_reason_code: Option<String>,
317}
318
319#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
321#[serde(rename_all = "camelCase")]
322pub struct DiagnosticQueryFailure {
323 pub id: String,
324 pub tables: Vec<String>,
326 pub code: String,
328 #[serde(skip_serializing_if = "Option::is_none")]
330 pub sqlite_code: Option<i32>,
331 pub at_ms: i64,
333}
334
335#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
336#[serde(rename_all = "camelCase")]
337pub struct ClientDiagnosticsSnapshot {
338 pub version: u8,
339 pub captured_at_ms: i64,
340 pub host: ClientDiagnosticsHost,
341 pub security_lifecycle: String,
342 pub schema: ClientDiagnosticsSchema,
343 pub replica: ClientDiagnosticsReplica,
344 pub lease: ClientDiagnosticsLease,
345 pub subscriptions: Vec<DiagnosticSubscription>,
346 pub subscriptions_truncated: bool,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub last_round: Option<DiagnosticLastRound>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub last_change: Option<DiagnosticLastChange>,
351 pub storage: ClientDiagnosticsStorage,
352 pub query_failures: Vec<DiagnosticQueryFailure>,
353}
354
355#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
356#[serde(rename_all = "camelCase")]
357pub struct ClientDiagnosticsSchema {
358 pub current_version: i32,
359 pub upgrading: bool,
360 #[serde(skip_serializing_if = "Option::is_none")]
361 pub required_version: Option<i32>,
362 #[serde(skip_serializing_if = "Option::is_none")]
363 pub latest_version: Option<i32>,
364}
365
366#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
367#[serde(rename_all = "camelCase")]
368pub struct ClientDiagnosticsReplica {
369 pub local_revision: String,
370 pub sync_needed: bool,
371 pub pending_outbox: usize,
372}
373
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
376#[serde(rename_all = "lowercase")]
377pub enum RealtimePolicy {
378 Required,
379 #[default]
380 Optional,
381 Off,
382}
383
384impl RealtimePolicy {
385 #[must_use]
386 pub fn as_str(self) -> &'static str {
387 match self {
388 RealtimePolicy::Required => "required",
389 RealtimePolicy::Optional => "optional",
390 RealtimePolicy::Off => "off",
391 }
392 }
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
400#[serde(rename_all = "lowercase")]
401pub enum RealtimeState {
402 Connected,
403 Connecting,
404 #[default]
405 Disconnected,
406 Lost,
407 Refused,
408 Disabled,
409}
410
411#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
412#[serde(rename_all = "camelCase")]
413pub struct ClientDiagnosticsLease {
414 pub state: String,
415 #[serde(skip_serializing_if = "Option::is_none")]
416 pub expires_at_ms: Option<i64>,
417 #[serde(skip_serializing_if = "Option::is_none")]
418 pub error_code: Option<String>,
419}
420
421#[derive(Debug, Clone, Serialize)]
423#[serde(rename_all = "camelCase")]
424pub struct ClientChangeBatch {
425 pub revision: String,
426 pub tables: Vec<TableChange>,
427 pub windows: Vec<WindowChange>,
428 #[serde(skip_serializing_if = "Option::is_none")]
429 pub status: Option<SyncStatusSnapshot>,
430 pub conflicts_changed: bool,
431 pub rejections_changed: bool,
432 pub outcomes_changed: bool,
433}
434
435#[derive(Debug, Clone, Serialize)]
436#[serde(tag = "kind", rename_all = "camelCase")]
437pub enum SyncIntent {
438 None,
439 Interactive,
440 Background {
441 #[serde(rename = "delayMs")]
442 delay_ms: u64,
443 },
444}
445
446#[derive(Debug, Clone, Serialize)]
447pub struct CommandEffects {
448 pub sync: SyncIntent,
449}
450
451impl CommandEffects {
452 #[must_use]
453 pub fn none() -> Self {
454 Self {
455 sync: SyncIntent::None,
456 }
457 }
458
459 #[must_use]
460 pub fn interactive() -> Self {
461 Self {
462 sync: SyncIntent::Interactive,
463 }
464 }
465}
466
467#[derive(Debug, Clone)]
468pub struct WindowCoverage {
469 pub base: WindowBase,
470 pub units: Vec<String>,
471}
472
473#[derive(Debug, Clone, Serialize)]
474#[serde(rename_all = "camelCase")]
475pub struct WindowUnitRef {
476 pub base_key: String,
477 pub unit: String,
478}
479
480#[derive(Debug, Clone, Serialize)]
481#[serde(rename_all = "camelCase")]
482pub struct CoverageSnapshot {
483 pub complete: bool,
484 pub pending: Vec<WindowUnitRef>,
485 pub missing: Vec<WindowUnitRef>,
486}
487
488#[derive(Debug, Clone, Copy)]
491pub struct QueryOwner<'a> {
492 pub id: &'a str,
493 pub tables: &'a [&'a str],
494}
495
496#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct QueryReadFailure {
502 pub code: Option<&'static str>,
503 pub sqlite_code: Option<i32>,
504 pub sqlite_message: Option<String>,
505 pub rollback_failure: Option<Box<QueryReadFailure>>,
506 pub message: String,
507}
508
509impl std::fmt::Display for QueryReadFailure {
510 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
511 match self.code {
512 Some(code) => write!(f, "{code}: {}", self.message),
513 None => f.write_str(&self.message),
514 }
515 }
516}
517
518impl From<String> for QueryReadFailure {
519 fn from(message: String) -> Self {
520 Self {
521 code: None,
522 sqlite_code: None,
523 sqlite_message: None,
524 rollback_failure: None,
525 message,
526 }
527 }
528}
529
530impl From<rusqlite::Error> for QueryReadFailure {
531 fn from(error: rusqlite::Error) -> Self {
533 let sqlite_code = match &error {
534 rusqlite::Error::SqliteFailure(failure, _) => Some(failure.extended_code),
535 _ => None,
536 };
537 match sqlite_code.map(|code| code & 0xff) {
538 Some(11 | 26) => Self {
539 code: Some("client.storage_corrupt"),
540 sqlite_code,
541 sqlite_message: Some(error.to_string()),
542 rollback_failure: None,
543 message: "local SQLite storage is corrupt".to_owned(),
544 },
545 Some(13) => Self {
546 code: Some("client.storage_full"),
547 sqlite_code,
548 sqlite_message: Some(error.to_string()),
549 rollback_failure: None,
550 message: "local SQLite storage is full".to_owned(),
551 },
552 Some(10) => Self {
553 code: Some("client.storage_io"),
554 sqlite_code,
555 sqlite_message: Some(error.to_string()),
556 rollback_failure: None,
557 message: "local SQLite storage I/O failed".to_owned(),
558 },
559 Some(5 | 6) => Self {
563 code: Some("client.storage_busy"),
564 sqlite_code,
565 sqlite_message: Some(error.to_string()),
566 rollback_failure: None,
567 message: "local SQLite storage is busy".to_owned(),
568 },
569 _ => Self {
570 code: None,
571 sqlite_code,
572 sqlite_message: Some(error.to_string()),
573 rollback_failure: None,
574 message: error.to_string(),
575 },
576 }
577 }
578}
579
580impl QueryReadFailure {
581 pub fn retryable(&self) -> bool {
584 self.code == Some("client.storage_busy")
585 }
586
587 pub fn details(&self) -> Option<Value> {
588 self.sqlite_code.map(|code| {
589 let mut details =
590 serde_json::json!({ "sqliteCode": code, "sqliteMessage": self.sqlite_message });
591 if let Some(rollback) = &self.rollback_failure {
592 details["rollbackFailure"] = serde_json::json!({
593 "sqliteCode": rollback.sqlite_code, "message": rollback.sqlite_message
594 });
595 }
596 details
597 })
598 }
599}
600
601#[derive(Debug, Clone, PartialEq, Eq)]
608pub struct ClientError {
609 pub code: String,
610 pub message: String,
611 pub details: Option<Value>,
612 pub retryable: bool,
613}
614
615const DEFAULT_CLIENT_ERROR_CODE: &str = "client.failed";
617
618const GENERIC_AUTHORING_MESSAGE: &str = "the local authoring operation failed";
620
621fn static_authoring_message(code: &str) -> Option<&'static str> {
624 match code {
625 "sync.unknown_table" => Some("the commit targets an unknown table"),
626 "sync.invalid_request" => Some("the authoring request is invalid"),
627 "sync.row_missing" => Some("a sparse patch requires a local row"),
628 "sync.constraint_violation" => Some("local write violates a unique constraint"),
629 DEFAULT_CLIENT_ERROR_CODE => Some(GENERIC_AUTHORING_MESSAGE),
630 _ => None,
631 }
632}
633
634fn is_client_code(candidate: &str) -> bool {
635 candidate.starts_with("client.") || candidate.starts_with("sync.")
636}
637
638impl std::fmt::Display for ClientError {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 write!(f, "{}: {}", self.code, self.message)
641 }
642}
643
644impl std::error::Error for ClientError {}
645
646impl From<QueryReadFailure> for ClientError {
647 fn from(failure: QueryReadFailure) -> Self {
648 match failure.code {
649 Some(code) => {
651 let details = failure.details();
652 let retryable = failure.retryable();
653 Self {
654 code: code.to_owned(),
655 message: failure.message,
656 details,
657 retryable,
658 }
659 }
660 None => {
661 let mut details = failure.details().unwrap_or_else(|| serde_json::json!({}));
662 if let Some(object) = details.as_object_mut() {
663 object.insert("legacyCause".to_owned(), Value::String(failure.message));
664 }
665 Self {
666 code: DEFAULT_CLIENT_ERROR_CODE.to_owned(),
667 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
668 details: Some(details),
669 retryable: false,
670 }
671 }
672 }
673 }
674}
675
676impl From<String> for ClientError {
677 fn from(message: String) -> Self {
682 if !message.contains(": ") && is_client_code(&message) {
684 return Self {
685 code: message.clone(),
686 message: static_authoring_message(&message)
687 .unwrap_or(GENERIC_AUTHORING_MESSAGE)
688 .to_owned(),
689 details: None,
690 retryable: false,
691 };
692 }
693 let (code, rest) = match message.split_once(": ") {
694 Some((code, rest)) if is_client_code(code) => (code, Some(rest)),
695 _ => (DEFAULT_CLIENT_ERROR_CODE, None),
696 };
697 match static_authoring_message(code) {
698 Some(fixed) => Self {
699 code: code.to_owned(),
700 message: fixed.to_owned(),
701 details: (rest != Some(fixed))
702 .then(|| serde_json::json!({ "legacyCause": message })),
703 retryable: false,
704 },
705 None => Self {
706 code: code.to_owned(),
707 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
708 details: Some(serde_json::json!({ "legacyCause": message })),
709 retryable: false,
710 },
711 }
712 }
713}
714
715#[derive(Debug, Clone, Serialize)]
716#[serde(rename_all = "camelCase")]
717pub struct QuerySnapshot {
718 pub revision: String,
719 pub rows: Vec<QueryRow>,
720 pub coverage: CoverageSnapshot,
721}
722
723impl WindowState {
724 #[must_use]
726 pub fn complete(&self, unit: &str) -> bool {
727 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
728 }
729}
730
731#[derive(Debug, Clone)]
733pub enum Mutation {
734 Patch {
735 table: String,
736 values: Map<String, Value>,
737 base_version: Option<i64>,
738 },
739 Upsert {
740 table: String,
741 values: Map<String, Value>,
742 base_version: Option<i64>,
743 },
744 Delete {
745 table: String,
746 row_id: String,
747 base_version: Option<i64>,
748 },
749}
750
751#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
752#[serde(rename_all = "camelCase")]
753pub struct SchemaFloor {
754 #[serde(skip_serializing_if = "Option::is_none")]
755 pub required_schema_version: Option<i32>,
756 #[serde(skip_serializing_if = "Option::is_none")]
757 pub latest_schema_version: Option<i32>,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
764#[serde(rename_all = "camelCase")]
765pub struct LeaseState {
766 #[serde(skip_serializing_if = "Option::is_none")]
767 pub lease_id: Option<String>,
768 #[serde(skip_serializing_if = "Option::is_none")]
769 pub expires_at_ms: Option<i64>,
770 #[serde(skip_serializing_if = "Option::is_none")]
771 pub error_code: Option<String>,
772}
773
774#[derive(Debug, Clone, Serialize)]
776#[serde(rename_all = "camelCase")]
777pub struct PresencePeer {
778 pub actor_id: String,
779 pub client_id: String,
780 pub doc: serde_json::Value,
781}
782
783#[derive(Debug, Clone, Serialize, Default)]
784#[serde(rename_all = "camelCase")]
785pub struct SyncReport {
786 pub pushed: u32,
787 pub applied: Vec<String>,
788 pub rejected: Vec<String>,
789 pub retryable: Vec<String>,
790 pub conflicts: u32,
791 pub commits_applied: u32,
792 pub segment_rows_applied: u32,
793 pub bootstrapping: Vec<String>,
794 pub resets: Vec<String>,
795 pub revoked: Vec<String>,
796 pub failed: Vec<String>,
797 pub deferred_commits: usize,
798 #[serde(skip_serializing_if = "Option::is_none")]
799 pub schema_floor: Option<SchemaFloor>,
800}
801
802impl SyncReport {
803 pub fn merge(&mut self, report: &Self) {
808 self.pushed += report.pushed;
809 self.applied.extend(report.applied.iter().cloned());
810 self.rejected.extend(report.rejected.iter().cloned());
811 self.retryable.extend(report.retryable.iter().cloned());
812 self.conflicts += report.conflicts;
813 self.commits_applied += report.commits_applied;
814 self.segment_rows_applied += report.segment_rows_applied;
815 self.bootstrapping = report.bootstrapping.clone();
816 self.resets.extend(report.resets.iter().cloned());
817 self.revoked.extend(report.revoked.iter().cloned());
818 self.failed.extend(report.failed.iter().cloned());
819 self.deferred_commits = report.deferred_commits;
820 self.schema_floor = report.schema_floor.clone();
821 }
822}
823
824#[derive(Debug, Clone)]
827pub enum SyncOutcome {
828 Ok(SyncReport),
829 Failed {
830 error_code: String,
831 message: String,
832 details: Option<Value>,
833 recommended_action: Option<String>,
837 },
838 RealtimeUnavailable {
843 state: RealtimeState,
844 reason_code: Option<String>,
845 retry_delay_ms: u64,
846 },
847 BudgetExhausted(SyncReport),
851}
852
853impl SyncOutcome {
854 pub fn to_json(&self) -> Value {
855 match self {
856 SyncOutcome::Ok(report) => {
857 let mut map = Map::new();
858 map.insert("ok".to_owned(), Value::Bool(true));
859 map.insert(
860 "report".to_owned(),
861 serde_json::to_value(report).expect("report serializes"),
862 );
863 Value::Object(map)
864 }
865 SyncOutcome::Failed {
866 error_code,
867 message,
868 details,
869 recommended_action,
870 } => {
871 let mut map = Map::new();
872 map.insert("ok".to_owned(), Value::Bool(false));
873 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
874 map.insert("message".to_owned(), Value::from(message.clone()));
875 if let Some(details) = details {
876 map.insert("details".to_owned(), details.clone());
877 }
878 if let Some(recommended_action) = recommended_action {
879 map.insert(
880 "recommendedAction".to_owned(),
881 Value::from(recommended_action.clone()),
882 );
883 }
884 Value::Object(map)
885 }
886 SyncOutcome::RealtimeUnavailable {
887 state,
888 reason_code,
889 retry_delay_ms,
890 } => {
891 let mut map = Map::new();
892 map.insert("ok".to_owned(), Value::Bool(false));
893 map.insert(
894 "errorCode".to_owned(),
895 Value::from(REALTIME_UNAVAILABLE_CODE),
896 );
897 map.insert(
898 "message".to_owned(),
899 Value::from("realtime is required and the realtime socket is not connected"),
900 );
901 map.insert(
902 "realtimeState".to_owned(),
903 serde_json::to_value(state).expect("state serializes"),
904 );
905 if let Some(reason_code) = reason_code {
906 map.insert(
907 "realtimeReasonCode".to_owned(),
908 Value::from(reason_code.clone()),
909 );
910 }
911 map.insert("retryDelayMs".to_owned(), Value::from(*retry_delay_ms));
912 Value::Object(map)
913 }
914 SyncOutcome::BudgetExhausted(report) => {
915 let mut map = Map::new();
916 map.insert("ok".to_owned(), Value::Bool(true));
917 map.insert("budgetExhausted".to_owned(), Value::Bool(true));
918 map.insert(
919 "report".to_owned(),
920 serde_json::to_value(report).expect("report serializes"),
921 );
922 Value::Object(map)
923 }
924 }
925 }
926}
927
928#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
929#[serde(rename_all = "camelCase")]
930pub struct ConflictRecord {
931 pub client_commit_id: String,
932 pub op_index: i32,
933 pub table: String,
934 pub row_id: String,
935 pub code: String,
936 pub message: String,
937 pub server_version: i64,
938 pub server_row: Map<String, Value>,
941 #[serde(default, skip_serializing_if = "Vec::is_empty")]
946 pub conflict_columns: Vec<String>,
947 #[serde(skip_serializing_if = "Option::is_none")]
948 pub operation: Option<CommitOperation>,
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
954#[serde(rename_all = "camelCase", deny_unknown_fields)]
955pub struct RejectionDetails {
956 #[serde(skip_serializing_if = "Option::is_none")]
957 pub field_paths: Option<Vec<String>>,
958 #[serde(skip_serializing_if = "Option::is_none")]
959 pub reason: Option<String>,
960 #[serde(skip_serializing_if = "Option::is_none")]
961 pub required_action: Option<String>,
962 #[serde(skip_serializing_if = "Option::is_none")]
963 pub references: Option<BTreeMap<String, String>>,
964}
965
966impl RejectionDetails {
967 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
968 if raw.len() > 4_096 {
969 return Err("rejection details exceed 4096 encoded bytes".to_owned());
970 }
971 let details: Self = serde_json::from_str(raw)
972 .map_err(|error| format!("invalid rejection details: {error}"))?;
973 details.validate()?;
974 Ok(details)
975 }
976
977 fn validate(&self) -> Result<(), String> {
978 let mut members = 0;
979 if let Some(paths) = &self.field_paths {
980 members += 1;
981 if paths.is_empty() || paths.len() > 32 {
982 return Err("fieldPaths must contain 1-32 paths".to_owned());
983 }
984 let mut seen = std::collections::BTreeSet::new();
985 for path in paths {
986 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
987 {
988 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
989 }
990 }
991 }
992 if let Some(reason) = &self.reason {
993 members += 1;
994 if !valid_token(reason, 96) {
995 return Err("reason must be a lowercase stable token".to_owned());
996 }
997 }
998 if let Some(action) = &self.required_action {
999 members += 1;
1000 if !valid_token(action, 96) {
1001 return Err("requiredAction must be a lowercase stable token".to_owned());
1002 }
1003 }
1004 if let Some(references) = &self.references {
1005 members += 1;
1006 if references.is_empty() || references.len() > 16 {
1007 return Err("references must contain 1-16 entries".to_owned());
1008 }
1009 for (key, value) in references {
1010 if !valid_token(key, 64)
1011 || value.is_empty()
1012 || value.len() > 256
1013 || value.trim() != value
1014 || value.chars().any(char::is_control)
1015 {
1016 return Err("references contains an invalid key or value".to_owned());
1017 }
1018 }
1019 }
1020 if members == 0 {
1021 return Err("rejection details must not be empty".to_owned());
1022 }
1023 Ok(())
1024 }
1025}
1026
1027fn valid_identifier(segment: &str) -> bool {
1028 let mut chars = segment.chars();
1029 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
1030 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
1031}
1032
1033fn valid_token(token: &str, max: usize) -> bool {
1034 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
1035 {
1036 return false;
1037 }
1038 let mut previous_separator = false;
1039 for character in token.chars() {
1040 if character.is_ascii_lowercase() || character.is_ascii_digit() {
1041 previous_separator = false;
1042 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
1043 previous_separator = true;
1044 } else {
1045 return false;
1046 }
1047 }
1048 !previous_separator
1049}
1050
1051#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1052#[serde(rename_all = "camelCase")]
1053pub struct RejectionRecord {
1054 pub client_commit_id: String,
1055 pub op_index: i32,
1056 pub code: String,
1057 pub message: String,
1058 pub retryable: bool,
1059 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub details: Option<RejectionDetails>,
1061 #[serde(skip_serializing_if = "Option::is_none")]
1062 pub operation: Option<CommitOperation>,
1063}
1064
1065#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1067#[serde(rename_all = "camelCase")]
1068pub struct CommitOperation {
1069 pub table: String,
1070 pub row_id: String,
1071 pub op: String,
1072 #[serde(skip_serializing_if = "Option::is_none")]
1073 pub base_version: Option<i64>,
1074 #[serde(skip_serializing_if = "Option::is_none")]
1075 pub values: Option<Map<String, Value>>,
1076 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1078 pub insert: bool,
1079}
1080
1081#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1082#[serde(rename_all = "snake_case")]
1083pub enum CommitOutcomeStatus {
1084 Applied,
1085 Cached,
1086 Conflict,
1087 Rejected,
1088}
1089
1090#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1091#[serde(rename_all = "snake_case")]
1092pub enum CommitOutcomeResolution {
1093 Active,
1094 ResolvedKeepServer,
1095 Superseded,
1096 Dismissed,
1097}
1098
1099#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1100#[serde(tag = "status", rename_all = "snake_case")]
1101pub enum CommitOperationOutcome {
1102 Applied {
1103 #[serde(rename = "opIndex")]
1104 op_index: i32,
1105 },
1106 Conflict {
1107 conflict: ConflictRecord,
1108 },
1109 Error {
1110 rejection: RejectionRecord,
1111 },
1112}
1113
1114#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1115#[serde(rename_all = "camelCase")]
1116pub struct RetainedUniqueConflict {
1117 pub index: String,
1118 pub columns: Vec<String>,
1119 pub row_id: String,
1120 pub server_row: Map<String, Value>,
1121 pub server_version: i64,
1122}
1123
1124#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1125#[serde(rename_all = "camelCase")]
1126pub struct RetainedCommitRow {
1127 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1128 pub unique_conflicts: Vec<RetainedUniqueConflict>,
1129 pub table: String,
1130 pub row_id: String,
1131 pub local_row: Option<Map<String, Value>>,
1132 pub server_row: Option<Map<String, Value>>,
1133 pub server_version: Option<i64>,
1134}
1135
1136#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1137#[serde(rename_all = "camelCase")]
1138pub struct CommitOutcome {
1139 pub sequence: i64,
1140 pub client_commit_id: String,
1141 pub status: CommitOutcomeStatus,
1142 pub recorded_at_ms: i64,
1143 pub results: Vec<CommitOperationOutcome>,
1144 #[serde(skip_serializing_if = "Option::is_none")]
1147 pub operations: Option<Vec<CommitOperation>>,
1148 #[serde(skip_serializing_if = "Option::is_none")]
1149 pub retained_rows: Option<Vec<RetainedCommitRow>>,
1150 pub resolution: CommitOutcomeResolution,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1152 pub resolved_at_ms: Option<i64>,
1153 #[serde(skip_serializing_if = "Option::is_none")]
1154 pub replacement_client_commit_id: Option<String>,
1155}
1156
1157#[derive(Debug, Clone, Deserialize, Default)]
1158#[serde(rename_all = "camelCase")]
1159pub struct CommitOutcomeQuery {
1160 pub limit: Option<usize>,
1161 #[serde(default)]
1162 pub active_only: bool,
1163}
1164
1165#[derive(Debug, Clone, Serialize)]
1174#[serde(
1175 tag = "state",
1176 rename_all = "camelCase",
1177 rename_all_fields = "camelCase"
1178)]
1179pub enum SubscriptionCatchup {
1180 Unknown { id: String },
1182 Known {
1183 id: String,
1184 table: String,
1185 status: String,
1187 cursor: i64,
1188 has_resume_token: bool,
1189 bootstrap_complete: bool,
1193 known_pending_pages: bool,
1198 #[serde(skip_serializing_if = "Option::is_none")]
1199 effective_scopes: Option<Value>,
1200 #[serde(skip_serializing_if = "Option::is_none")]
1201 reason_code: Option<String>,
1202 },
1203}
1204
1205#[derive(Debug, Clone, Serialize)]
1214#[serde(tag = "status", rename_all = "snake_case")]
1215pub enum CommitDelivery {
1216 Pending {
1217 #[serde(rename = "clientCommitId")]
1218 client_commit_id: String,
1219 },
1220 Known {
1221 #[serde(rename = "clientCommitId")]
1222 client_commit_id: String,
1223 outcome: CommitOutcome,
1224 },
1225 Unknown {
1226 #[serde(rename = "clientCommitId")]
1227 client_commit_id: String,
1228 },
1229}
1230
1231#[derive(Debug, Clone, Serialize)]
1236#[serde(rename_all = "camelCase")]
1237pub struct SnapshotRead {
1238 pub revision: String,
1239 pub queries: Vec<Vec<QueryRow>>,
1240 pub coverage: CoverageSnapshot,
1241 pub subscriptions: Vec<SubscriptionCatchup>,
1242 pub deliveries: Vec<CommitDelivery>,
1243}
1244
1245#[derive(Debug, Clone, Deserialize)]
1246#[serde(rename_all = "camelCase")]
1247pub struct ResolveCommitOutcomeInput {
1248 pub client_commit_id: String,
1249 pub resolution: CommitOutcomeResolution,
1250 pub replacement_client_commit_id: Option<String>,
1251}
1252
1253#[derive(Debug, Clone, Serialize)]
1254#[serde(rename_all = "camelCase")]
1255pub struct RowState {
1256 pub row_id: String,
1257 pub version: i64,
1260 pub values: Map<String, Value>,
1261}
1262
1263#[derive(Debug, Clone, Serialize)]
1264#[serde(rename_all = "camelCase")]
1265pub struct SubscriptionStateView {
1266 pub id: String,
1267 pub table: String,
1268 pub status: String,
1270 pub cursor: i64,
1271 pub has_resume_token: bool,
1272 #[serde(skip_serializing_if = "Option::is_none")]
1273 pub effective_scopes: Option<Value>,
1274 #[serde(skip_serializing_if = "Option::is_none")]
1275 pub reason_code: Option<String>,
1276}
1277
1278#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1282#[serde(rename_all = "camelCase", deny_unknown_fields)]
1283pub struct LocalDataPurgeTarget {
1284 pub table: String,
1285 pub selectors: BTreeMap<String, Vec<String>>,
1286}
1287
1288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1291#[serde(rename_all = "camelCase", deny_unknown_fields)]
1292pub struct LocalDataPurgeInput {
1293 pub purge_id: String,
1294 pub targets: Vec<LocalDataPurgeTarget>,
1295}
1296
1297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1299#[serde(rename_all = "camelCase")]
1300pub struct LocalDataPurgeResult {
1301 pub already_applied: bool,
1302 pub purged_rows: usize,
1303 pub dropped_commits: usize,
1304}
1305
1306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1308#[serde(rename_all = "camelCase", deny_unknown_fields)]
1309pub struct LocalDataRebootstrapInput {
1310 pub rebootstrap_id: String,
1311}
1312
1313#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1315#[serde(rename_all = "camelCase")]
1316pub struct LocalDataRebootstrapResult {
1317 pub already_applied: bool,
1318 pub retained_commits: usize,
1319 pub reset_subscriptions: usize,
1320}
1321
1322#[derive(Debug, Clone, Default)]
1324pub struct ClientLimits {
1325 pub limit_commits: Option<i32>,
1326 pub limit_snapshot_rows: Option<i32>,
1327 pub max_snapshot_pages: Option<i32>,
1328 pub accept: Option<u8>,
1331 pub blob_cache_max_bytes: Option<i64>,
1336 pub outcome_retention_max_entries: Option<usize>,
1339 pub max_push_commits_per_request: Option<usize>,
1341 pub max_push_operations_per_request: Option<usize>,
1346 pub max_push_request_bytes: Option<usize>,
1352 pub previous_version_context: Option<PreviousVersionContextConfig>,
1356 pub authority_reads: Vec<AuthorityReadDeclaration>,
1358}
1359
1360#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1362#[serde(rename_all = "camelCase", deny_unknown_fields)]
1363pub struct AuthorityReadDeclaration {
1364 pub table: String,
1365 pub columns: Vec<String>,
1366 pub scopes: std::collections::BTreeMap<String, Vec<String>>,
1367}