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";
211
212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
213#[serde(rename_all = "camelCase", deny_unknown_fields)]
214pub struct ExpectedDiagnosticSubscription {
215 pub id: String,
216 pub table: String,
217}
218
219#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
220#[serde(rename_all = "camelCase", deny_unknown_fields)]
221pub struct ClientDiagnosticsRequest {
222 #[serde(default)]
223 pub expected_subscriptions: Vec<ExpectedDiagnosticSubscription>,
224}
225
226#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
227#[serde(rename_all = "camelCase")]
228pub struct ClientDiagnosticsHost {
229 pub kind: String,
230 pub role: String,
231 pub connectivity: String,
232 pub realtime: RealtimeState,
233 pub realtime_policy: RealtimePolicy,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub realtime_reason_code: Option<String>,
236 #[serde(skip_serializing_if = "Option::is_none")]
237 pub realtime_retry_delay_ms: Option<u64>,
238}
239
240#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
241#[serde(rename_all = "camelCase")]
242pub struct DiagnosticSubscription {
243 pub id: String,
244 pub table: String,
245 pub state: String,
246 pub complete: bool,
247 #[serde(skip_serializing_if = "Option::is_none")]
248 pub cursor: Option<i64>,
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub reason_code: Option<String>,
251}
252
253#[derive(Debug, Clone, Serialize, Default, PartialEq, Eq)]
254#[serde(rename_all = "camelCase")]
255pub struct DiagnosticRoundCounters {
256 pub pushed: u32,
257 pub applied: usize,
258 pub rejected: usize,
259 pub retryable: usize,
260 pub conflicts: u32,
261 pub commits_applied: u32,
262 pub segment_rows_applied: u32,
263 pub bootstrapping: usize,
264 pub resets: usize,
265 pub revoked: usize,
266 pub failed: usize,
267 pub deferred_commits: usize,
268}
269
270#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
271#[serde(rename_all = "camelCase")]
272pub struct DiagnosticLastRound {
273 pub status: String,
274 pub started_at_ms: i64,
275 pub completed_at_ms: i64,
276 pub duration_ms: i64,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 pub counters: Option<DiagnosticRoundCounters>,
279 #[serde(skip_serializing_if = "Option::is_none")]
280 pub error_code: Option<String>,
281}
282
283#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
284#[serde(rename_all = "camelCase")]
285pub struct DiagnosticLastChange {
286 pub revision: String,
287 pub recorded_at_ms: i64,
288 pub tables: Vec<String>,
289 pub windows: Vec<String>,
290 pub domains_truncated: bool,
291 pub status_changed: bool,
292 pub conflicts_changed: bool,
293 pub rejections_changed: bool,
294 pub outcomes_changed: bool,
295}
296
297#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
298#[serde(rename_all = "camelCase")]
299pub struct ClientDiagnosticsStorage {
300 pub status: String,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub database_bytes_approx: Option<i64>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub pending_outbox_bytes_approx: Option<i64>,
305 #[serde(skip_serializing_if = "Option::is_none")]
306 pub retained_outcome_bytes_approx: Option<i64>,
307 #[serde(skip_serializing_if = "Option::is_none")]
308 pub retained_outcome_entries: Option<i64>,
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub blob_cache_bytes_approx: Option<i64>,
311 #[serde(skip_serializing_if = "Option::is_none")]
312 pub pressure_reason_code: Option<String>,
313}
314
315#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
317#[serde(rename_all = "camelCase")]
318pub struct DiagnosticQueryFailure {
319 pub id: String,
320 pub tables: Vec<String>,
322 pub code: String,
324 #[serde(skip_serializing_if = "Option::is_none")]
326 pub sqlite_code: Option<i32>,
327 pub at_ms: i64,
329}
330
331#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
332#[serde(rename_all = "camelCase")]
333pub struct ClientDiagnosticsSnapshot {
334 pub version: u8,
335 pub captured_at_ms: i64,
336 pub host: ClientDiagnosticsHost,
337 pub security_lifecycle: String,
338 pub schema: ClientDiagnosticsSchema,
339 pub replica: ClientDiagnosticsReplica,
340 pub lease: ClientDiagnosticsLease,
341 pub subscriptions: Vec<DiagnosticSubscription>,
342 pub subscriptions_truncated: bool,
343 #[serde(skip_serializing_if = "Option::is_none")]
344 pub last_round: Option<DiagnosticLastRound>,
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub last_change: Option<DiagnosticLastChange>,
347 pub storage: ClientDiagnosticsStorage,
348 pub query_failures: Vec<DiagnosticQueryFailure>,
349}
350
351#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
352#[serde(rename_all = "camelCase")]
353pub struct ClientDiagnosticsSchema {
354 pub current_version: i32,
355 pub upgrading: bool,
356 #[serde(skip_serializing_if = "Option::is_none")]
357 pub required_version: Option<i32>,
358 #[serde(skip_serializing_if = "Option::is_none")]
359 pub latest_version: Option<i32>,
360}
361
362#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
363#[serde(rename_all = "camelCase")]
364pub struct ClientDiagnosticsReplica {
365 pub local_revision: String,
366 pub sync_needed: bool,
367 pub pending_outbox: usize,
368}
369
370#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
372#[serde(rename_all = "lowercase")]
373pub enum RealtimePolicy {
374 Required,
375 #[default]
376 Optional,
377 Off,
378}
379
380impl RealtimePolicy {
381 #[must_use]
382 pub fn as_str(self) -> &'static str {
383 match self {
384 RealtimePolicy::Required => "required",
385 RealtimePolicy::Optional => "optional",
386 RealtimePolicy::Off => "off",
387 }
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
396#[serde(rename_all = "lowercase")]
397pub enum RealtimeState {
398 Connected,
399 Connecting,
400 #[default]
401 Disconnected,
402 Lost,
403 Refused,
404 Disabled,
405}
406
407#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
408#[serde(rename_all = "camelCase")]
409pub struct ClientDiagnosticsLease {
410 pub state: String,
411 #[serde(skip_serializing_if = "Option::is_none")]
412 pub expires_at_ms: Option<i64>,
413 #[serde(skip_serializing_if = "Option::is_none")]
414 pub error_code: Option<String>,
415}
416
417#[derive(Debug, Clone, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct ClientChangeBatch {
421 pub revision: String,
422 pub tables: Vec<TableChange>,
423 pub windows: Vec<WindowChange>,
424 #[serde(skip_serializing_if = "Option::is_none")]
425 pub status: Option<SyncStatusSnapshot>,
426 pub conflicts_changed: bool,
427 pub rejections_changed: bool,
428 pub outcomes_changed: bool,
429}
430
431#[derive(Debug, Clone, Serialize)]
432#[serde(tag = "kind", rename_all = "camelCase")]
433pub enum SyncIntent {
434 None,
435 Interactive,
436 Background {
437 #[serde(rename = "delayMs")]
438 delay_ms: u64,
439 },
440}
441
442#[derive(Debug, Clone, Serialize)]
443pub struct CommandEffects {
444 pub sync: SyncIntent,
445}
446
447impl CommandEffects {
448 #[must_use]
449 pub fn none() -> Self {
450 Self {
451 sync: SyncIntent::None,
452 }
453 }
454
455 #[must_use]
456 pub fn interactive() -> Self {
457 Self {
458 sync: SyncIntent::Interactive,
459 }
460 }
461}
462
463#[derive(Debug, Clone)]
464pub struct WindowCoverage {
465 pub base: WindowBase,
466 pub units: Vec<String>,
467}
468
469#[derive(Debug, Clone, Serialize)]
470#[serde(rename_all = "camelCase")]
471pub struct WindowUnitRef {
472 pub base_key: String,
473 pub unit: String,
474}
475
476#[derive(Debug, Clone, Serialize)]
477#[serde(rename_all = "camelCase")]
478pub struct CoverageSnapshot {
479 pub complete: bool,
480 pub pending: Vec<WindowUnitRef>,
481 pub missing: Vec<WindowUnitRef>,
482}
483
484#[derive(Debug, Clone, Copy)]
487pub struct QueryOwner<'a> {
488 pub id: &'a str,
489 pub tables: &'a [&'a str],
490}
491
492#[derive(Debug, Clone, PartialEq, Eq)]
497pub struct QueryReadFailure {
498 pub code: Option<&'static str>,
499 pub sqlite_code: Option<i32>,
500 pub sqlite_message: Option<String>,
501 pub rollback_failure: Option<Box<QueryReadFailure>>,
502 pub message: String,
503}
504
505impl std::fmt::Display for QueryReadFailure {
506 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
507 match self.code {
508 Some(code) => write!(f, "{code}: {}", self.message),
509 None => f.write_str(&self.message),
510 }
511 }
512}
513
514impl From<String> for QueryReadFailure {
515 fn from(message: String) -> Self {
516 Self {
517 code: None,
518 sqlite_code: None,
519 sqlite_message: None,
520 rollback_failure: None,
521 message,
522 }
523 }
524}
525
526impl From<rusqlite::Error> for QueryReadFailure {
527 fn from(error: rusqlite::Error) -> Self {
529 let sqlite_code = match &error {
530 rusqlite::Error::SqliteFailure(failure, _) => Some(failure.extended_code),
531 _ => None,
532 };
533 match sqlite_code.map(|code| code & 0xff) {
534 Some(11 | 26) => Self {
535 code: Some("client.storage_corrupt"),
536 sqlite_code,
537 sqlite_message: Some(error.to_string()),
538 rollback_failure: None,
539 message: "local SQLite storage is corrupt".to_owned(),
540 },
541 Some(13) => Self {
542 code: Some("client.storage_full"),
543 sqlite_code,
544 sqlite_message: Some(error.to_string()),
545 rollback_failure: None,
546 message: "local SQLite storage is full".to_owned(),
547 },
548 Some(10) => Self {
549 code: Some("client.storage_io"),
550 sqlite_code,
551 sqlite_message: Some(error.to_string()),
552 rollback_failure: None,
553 message: "local SQLite storage I/O failed".to_owned(),
554 },
555 Some(5 | 6) => Self {
559 code: Some("client.storage_busy"),
560 sqlite_code,
561 sqlite_message: Some(error.to_string()),
562 rollback_failure: None,
563 message: "local SQLite storage is busy".to_owned(),
564 },
565 _ => Self {
566 code: None,
567 sqlite_code,
568 sqlite_message: Some(error.to_string()),
569 rollback_failure: None,
570 message: error.to_string(),
571 },
572 }
573 }
574}
575
576impl QueryReadFailure {
577 pub fn retryable(&self) -> bool {
580 self.code == Some("client.storage_busy")
581 }
582
583 pub fn details(&self) -> Option<Value> {
584 self.sqlite_code.map(|code| {
585 let mut details =
586 serde_json::json!({ "sqliteCode": code, "sqliteMessage": self.sqlite_message });
587 if let Some(rollback) = &self.rollback_failure {
588 details["rollbackFailure"] = serde_json::json!({
589 "sqliteCode": rollback.sqlite_code, "message": rollback.sqlite_message
590 });
591 }
592 details
593 })
594 }
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct ClientError {
605 pub code: String,
606 pub message: String,
607 pub details: Option<Value>,
608 pub retryable: bool,
609}
610
611const DEFAULT_CLIENT_ERROR_CODE: &str = "client.failed";
613
614const GENERIC_AUTHORING_MESSAGE: &str = "the local authoring operation failed";
616
617fn static_authoring_message(code: &str) -> Option<&'static str> {
620 match code {
621 "sync.unknown_table" => Some("the commit targets an unknown table"),
622 "sync.invalid_request" => Some("the authoring request is invalid"),
623 "sync.row_missing" => Some("a sparse patch requires a local row"),
624 "sync.constraint_violation" => Some("local write violates a unique constraint"),
625 DEFAULT_CLIENT_ERROR_CODE => Some(GENERIC_AUTHORING_MESSAGE),
626 _ => None,
627 }
628}
629
630fn is_client_code(candidate: &str) -> bool {
631 candidate.starts_with("client.") || candidate.starts_with("sync.")
632}
633
634impl std::fmt::Display for ClientError {
635 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 write!(f, "{}: {}", self.code, self.message)
637 }
638}
639
640impl std::error::Error for ClientError {}
641
642impl From<QueryReadFailure> for ClientError {
643 fn from(failure: QueryReadFailure) -> Self {
644 match failure.code {
645 Some(code) => {
647 let details = failure.details();
648 let retryable = failure.retryable();
649 Self {
650 code: code.to_owned(),
651 message: failure.message,
652 details,
653 retryable,
654 }
655 }
656 None => {
657 let mut details = failure.details().unwrap_or_else(|| serde_json::json!({}));
658 if let Some(object) = details.as_object_mut() {
659 object.insert("legacyCause".to_owned(), Value::String(failure.message));
660 }
661 Self {
662 code: DEFAULT_CLIENT_ERROR_CODE.to_owned(),
663 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
664 details: Some(details),
665 retryable: false,
666 }
667 }
668 }
669 }
670}
671
672impl From<String> for ClientError {
673 fn from(message: String) -> Self {
678 if !message.contains(": ") && is_client_code(&message) {
680 return Self {
681 code: message.clone(),
682 message: static_authoring_message(&message)
683 .unwrap_or(GENERIC_AUTHORING_MESSAGE)
684 .to_owned(),
685 details: None,
686 retryable: false,
687 };
688 }
689 let (code, rest) = match message.split_once(": ") {
690 Some((code, rest)) if is_client_code(code) => (code, Some(rest)),
691 _ => (DEFAULT_CLIENT_ERROR_CODE, None),
692 };
693 match static_authoring_message(code) {
694 Some(fixed) => Self {
695 code: code.to_owned(),
696 message: fixed.to_owned(),
697 details: (rest != Some(fixed))
698 .then(|| serde_json::json!({ "legacyCause": message })),
699 retryable: false,
700 },
701 None => Self {
702 code: code.to_owned(),
703 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
704 details: Some(serde_json::json!({ "legacyCause": message })),
705 retryable: false,
706 },
707 }
708 }
709}
710
711#[derive(Debug, Clone, Serialize)]
712#[serde(rename_all = "camelCase")]
713pub struct QuerySnapshot {
714 pub revision: String,
715 pub rows: Vec<QueryRow>,
716 pub coverage: CoverageSnapshot,
717}
718
719impl WindowState {
720 #[must_use]
722 pub fn complete(&self, unit: &str) -> bool {
723 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
724 }
725}
726
727#[derive(Debug, Clone)]
729pub enum Mutation {
730 Patch {
731 table: String,
732 values: Map<String, Value>,
733 base_version: Option<i64>,
734 },
735 Upsert {
736 table: String,
737 values: Map<String, Value>,
738 base_version: Option<i64>,
739 },
740 Delete {
741 table: String,
742 row_id: String,
743 base_version: Option<i64>,
744 },
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
748#[serde(rename_all = "camelCase")]
749pub struct SchemaFloor {
750 #[serde(skip_serializing_if = "Option::is_none")]
751 pub required_schema_version: Option<i32>,
752 #[serde(skip_serializing_if = "Option::is_none")]
753 pub latest_schema_version: Option<i32>,
754}
755
756#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
760#[serde(rename_all = "camelCase")]
761pub struct LeaseState {
762 #[serde(skip_serializing_if = "Option::is_none")]
763 pub lease_id: Option<String>,
764 #[serde(skip_serializing_if = "Option::is_none")]
765 pub expires_at_ms: Option<i64>,
766 #[serde(skip_serializing_if = "Option::is_none")]
767 pub error_code: Option<String>,
768}
769
770#[derive(Debug, Clone, Serialize)]
772#[serde(rename_all = "camelCase")]
773pub struct PresencePeer {
774 pub actor_id: String,
775 pub client_id: String,
776 pub doc: serde_json::Value,
777}
778
779#[derive(Debug, Clone, Serialize, Default)]
780#[serde(rename_all = "camelCase")]
781pub struct SyncReport {
782 pub pushed: u32,
783 pub applied: Vec<String>,
784 pub rejected: Vec<String>,
785 pub retryable: Vec<String>,
786 pub conflicts: u32,
787 pub commits_applied: u32,
788 pub segment_rows_applied: u32,
789 pub bootstrapping: Vec<String>,
790 pub resets: Vec<String>,
791 pub revoked: Vec<String>,
792 pub failed: Vec<String>,
793 pub deferred_commits: usize,
794 #[serde(skip_serializing_if = "Option::is_none")]
795 pub schema_floor: Option<SchemaFloor>,
796}
797
798impl SyncReport {
799 pub fn merge(&mut self, report: &Self) {
804 self.pushed += report.pushed;
805 self.applied.extend(report.applied.iter().cloned());
806 self.rejected.extend(report.rejected.iter().cloned());
807 self.retryable.extend(report.retryable.iter().cloned());
808 self.conflicts += report.conflicts;
809 self.commits_applied += report.commits_applied;
810 self.segment_rows_applied += report.segment_rows_applied;
811 self.bootstrapping = report.bootstrapping.clone();
812 self.resets.extend(report.resets.iter().cloned());
813 self.revoked.extend(report.revoked.iter().cloned());
814 self.failed.extend(report.failed.iter().cloned());
815 self.deferred_commits = report.deferred_commits;
816 self.schema_floor = report.schema_floor.clone();
817 }
818}
819
820#[derive(Debug, Clone)]
823pub enum SyncOutcome {
824 Ok(SyncReport),
825 Failed {
826 error_code: String,
827 message: String,
828 details: Option<Value>,
829 },
830 RealtimeUnavailable {
835 state: RealtimeState,
836 reason_code: Option<String>,
837 retry_delay_ms: u64,
838 },
839 BudgetExhausted(SyncReport),
843}
844
845impl SyncOutcome {
846 pub fn to_json(&self) -> Value {
847 match self {
848 SyncOutcome::Ok(report) => {
849 let mut map = Map::new();
850 map.insert("ok".to_owned(), Value::Bool(true));
851 map.insert(
852 "report".to_owned(),
853 serde_json::to_value(report).expect("report serializes"),
854 );
855 Value::Object(map)
856 }
857 SyncOutcome::Failed {
858 error_code,
859 message,
860 details,
861 } => {
862 let mut map = Map::new();
863 map.insert("ok".to_owned(), Value::Bool(false));
864 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
865 map.insert("message".to_owned(), Value::from(message.clone()));
866 if let Some(details) = details {
867 map.insert("details".to_owned(), details.clone());
868 }
869 Value::Object(map)
870 }
871 SyncOutcome::RealtimeUnavailable {
872 state,
873 reason_code,
874 retry_delay_ms,
875 } => {
876 let mut map = Map::new();
877 map.insert("ok".to_owned(), Value::Bool(false));
878 map.insert(
879 "errorCode".to_owned(),
880 Value::from(REALTIME_UNAVAILABLE_CODE),
881 );
882 map.insert(
883 "message".to_owned(),
884 Value::from("realtime is required and the realtime socket is not connected"),
885 );
886 map.insert(
887 "realtimeState".to_owned(),
888 serde_json::to_value(state).expect("state serializes"),
889 );
890 if let Some(reason_code) = reason_code {
891 map.insert(
892 "realtimeReasonCode".to_owned(),
893 Value::from(reason_code.clone()),
894 );
895 }
896 map.insert("retryDelayMs".to_owned(), Value::from(*retry_delay_ms));
897 Value::Object(map)
898 }
899 SyncOutcome::BudgetExhausted(report) => {
900 let mut map = Map::new();
901 map.insert("ok".to_owned(), Value::Bool(true));
902 map.insert("budgetExhausted".to_owned(), Value::Bool(true));
903 map.insert(
904 "report".to_owned(),
905 serde_json::to_value(report).expect("report serializes"),
906 );
907 Value::Object(map)
908 }
909 }
910 }
911}
912
913#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
914#[serde(rename_all = "camelCase")]
915pub struct ConflictRecord {
916 pub client_commit_id: String,
917 pub op_index: i32,
918 pub table: String,
919 pub row_id: String,
920 pub code: String,
921 pub message: String,
922 pub server_version: i64,
923 pub server_row: Map<String, Value>,
926 #[serde(default, skip_serializing_if = "Vec::is_empty")]
931 pub conflict_columns: Vec<String>,
932 #[serde(skip_serializing_if = "Option::is_none")]
933 pub operation: Option<CommitOperation>,
934}
935
936#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
939#[serde(rename_all = "camelCase", deny_unknown_fields)]
940pub struct RejectionDetails {
941 #[serde(skip_serializing_if = "Option::is_none")]
942 pub field_paths: Option<Vec<String>>,
943 #[serde(skip_serializing_if = "Option::is_none")]
944 pub reason: Option<String>,
945 #[serde(skip_serializing_if = "Option::is_none")]
946 pub required_action: Option<String>,
947 #[serde(skip_serializing_if = "Option::is_none")]
948 pub references: Option<BTreeMap<String, String>>,
949}
950
951impl RejectionDetails {
952 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
953 if raw.len() > 4_096 {
954 return Err("rejection details exceed 4096 encoded bytes".to_owned());
955 }
956 let details: Self = serde_json::from_str(raw)
957 .map_err(|error| format!("invalid rejection details: {error}"))?;
958 details.validate()?;
959 Ok(details)
960 }
961
962 fn validate(&self) -> Result<(), String> {
963 let mut members = 0;
964 if let Some(paths) = &self.field_paths {
965 members += 1;
966 if paths.is_empty() || paths.len() > 32 {
967 return Err("fieldPaths must contain 1-32 paths".to_owned());
968 }
969 let mut seen = std::collections::BTreeSet::new();
970 for path in paths {
971 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
972 {
973 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
974 }
975 }
976 }
977 if let Some(reason) = &self.reason {
978 members += 1;
979 if !valid_token(reason, 96) {
980 return Err("reason must be a lowercase stable token".to_owned());
981 }
982 }
983 if let Some(action) = &self.required_action {
984 members += 1;
985 if !valid_token(action, 96) {
986 return Err("requiredAction must be a lowercase stable token".to_owned());
987 }
988 }
989 if let Some(references) = &self.references {
990 members += 1;
991 if references.is_empty() || references.len() > 16 {
992 return Err("references must contain 1-16 entries".to_owned());
993 }
994 for (key, value) in references {
995 if !valid_token(key, 64)
996 || value.is_empty()
997 || value.len() > 256
998 || value.trim() != value
999 || value.chars().any(char::is_control)
1000 {
1001 return Err("references contains an invalid key or value".to_owned());
1002 }
1003 }
1004 }
1005 if members == 0 {
1006 return Err("rejection details must not be empty".to_owned());
1007 }
1008 Ok(())
1009 }
1010}
1011
1012fn valid_identifier(segment: &str) -> bool {
1013 let mut chars = segment.chars();
1014 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
1015 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
1016}
1017
1018fn valid_token(token: &str, max: usize) -> bool {
1019 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
1020 {
1021 return false;
1022 }
1023 let mut previous_separator = false;
1024 for character in token.chars() {
1025 if character.is_ascii_lowercase() || character.is_ascii_digit() {
1026 previous_separator = false;
1027 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
1028 previous_separator = true;
1029 } else {
1030 return false;
1031 }
1032 }
1033 !previous_separator
1034}
1035
1036#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1037#[serde(rename_all = "camelCase")]
1038pub struct RejectionRecord {
1039 pub client_commit_id: String,
1040 pub op_index: i32,
1041 pub code: String,
1042 pub message: String,
1043 pub retryable: bool,
1044 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub details: Option<RejectionDetails>,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1047 pub operation: Option<CommitOperation>,
1048}
1049
1050#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1052#[serde(rename_all = "camelCase")]
1053pub struct CommitOperation {
1054 pub table: String,
1055 pub row_id: String,
1056 pub op: String,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub base_version: Option<i64>,
1059 #[serde(skip_serializing_if = "Option::is_none")]
1060 pub values: Option<Map<String, Value>>,
1061}
1062
1063#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1064#[serde(rename_all = "snake_case")]
1065pub enum CommitOutcomeStatus {
1066 Applied,
1067 Cached,
1068 Conflict,
1069 Rejected,
1070}
1071
1072#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1073#[serde(rename_all = "snake_case")]
1074pub enum CommitOutcomeResolution {
1075 Active,
1076 ResolvedKeepServer,
1077 Superseded,
1078 Dismissed,
1079}
1080
1081#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1082#[serde(tag = "status", rename_all = "snake_case")]
1083pub enum CommitOperationOutcome {
1084 Applied {
1085 #[serde(rename = "opIndex")]
1086 op_index: i32,
1087 },
1088 Conflict {
1089 conflict: ConflictRecord,
1090 },
1091 Error {
1092 rejection: RejectionRecord,
1093 },
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1097#[serde(rename_all = "camelCase")]
1098pub struct RetainedUniqueConflict {
1099 pub index: String,
1100 pub columns: Vec<String>,
1101 pub row_id: String,
1102 pub server_row: Map<String, Value>,
1103 pub server_version: i64,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1107#[serde(rename_all = "camelCase")]
1108pub struct RetainedCommitRow {
1109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1110 pub unique_conflicts: Vec<RetainedUniqueConflict>,
1111 pub table: String,
1112 pub row_id: String,
1113 pub local_row: Option<Map<String, Value>>,
1114 pub server_row: Option<Map<String, Value>>,
1115 pub server_version: Option<i64>,
1116}
1117
1118#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1119#[serde(rename_all = "camelCase")]
1120pub struct CommitOutcome {
1121 pub sequence: i64,
1122 pub client_commit_id: String,
1123 pub status: CommitOutcomeStatus,
1124 pub recorded_at_ms: i64,
1125 pub results: Vec<CommitOperationOutcome>,
1126 #[serde(skip_serializing_if = "Option::is_none")]
1129 pub operations: Option<Vec<CommitOperation>>,
1130 #[serde(skip_serializing_if = "Option::is_none")]
1131 pub retained_rows: Option<Vec<RetainedCommitRow>>,
1132 pub resolution: CommitOutcomeResolution,
1133 #[serde(skip_serializing_if = "Option::is_none")]
1134 pub resolved_at_ms: Option<i64>,
1135 #[serde(skip_serializing_if = "Option::is_none")]
1136 pub replacement_client_commit_id: Option<String>,
1137}
1138
1139#[derive(Debug, Clone, Deserialize, Default)]
1140#[serde(rename_all = "camelCase")]
1141pub struct CommitOutcomeQuery {
1142 pub limit: Option<usize>,
1143 #[serde(default)]
1144 pub active_only: bool,
1145}
1146
1147#[derive(Debug, Clone, Serialize)]
1156#[serde(
1157 tag = "state",
1158 rename_all = "camelCase",
1159 rename_all_fields = "camelCase"
1160)]
1161pub enum SubscriptionCatchup {
1162 Unknown { id: String },
1164 Known {
1165 id: String,
1166 table: String,
1167 status: String,
1169 cursor: i64,
1170 has_resume_token: bool,
1171 bootstrap_complete: bool,
1175 known_pending_pages: bool,
1180 #[serde(skip_serializing_if = "Option::is_none")]
1181 effective_scopes: Option<Value>,
1182 #[serde(skip_serializing_if = "Option::is_none")]
1183 reason_code: Option<String>,
1184 },
1185}
1186
1187#[derive(Debug, Clone, Serialize)]
1196#[serde(tag = "status", rename_all = "snake_case")]
1197pub enum CommitDelivery {
1198 Pending {
1199 #[serde(rename = "clientCommitId")]
1200 client_commit_id: String,
1201 },
1202 Known {
1203 #[serde(rename = "clientCommitId")]
1204 client_commit_id: String,
1205 outcome: CommitOutcome,
1206 },
1207 Unknown {
1208 #[serde(rename = "clientCommitId")]
1209 client_commit_id: String,
1210 },
1211}
1212
1213#[derive(Debug, Clone, Serialize)]
1218#[serde(rename_all = "camelCase")]
1219pub struct SnapshotRead {
1220 pub revision: String,
1221 pub queries: Vec<Vec<QueryRow>>,
1222 pub coverage: CoverageSnapshot,
1223 pub subscriptions: Vec<SubscriptionCatchup>,
1224 pub deliveries: Vec<CommitDelivery>,
1225}
1226
1227#[derive(Debug, Clone, Deserialize)]
1228#[serde(rename_all = "camelCase")]
1229pub struct ResolveCommitOutcomeInput {
1230 pub client_commit_id: String,
1231 pub resolution: CommitOutcomeResolution,
1232 pub replacement_client_commit_id: Option<String>,
1233}
1234
1235#[derive(Debug, Clone, Serialize)]
1236#[serde(rename_all = "camelCase")]
1237pub struct RowState {
1238 pub row_id: String,
1239 pub version: i64,
1242 pub values: Map<String, Value>,
1243}
1244
1245#[derive(Debug, Clone, Serialize)]
1246#[serde(rename_all = "camelCase")]
1247pub struct SubscriptionStateView {
1248 pub id: String,
1249 pub table: String,
1250 pub status: String,
1252 pub cursor: i64,
1253 pub has_resume_token: bool,
1254 #[serde(skip_serializing_if = "Option::is_none")]
1255 pub effective_scopes: Option<Value>,
1256 #[serde(skip_serializing_if = "Option::is_none")]
1257 pub reason_code: Option<String>,
1258}
1259
1260#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1264#[serde(rename_all = "camelCase", deny_unknown_fields)]
1265pub struct LocalDataPurgeTarget {
1266 pub table: String,
1267 pub selectors: BTreeMap<String, Vec<String>>,
1268}
1269
1270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1273#[serde(rename_all = "camelCase", deny_unknown_fields)]
1274pub struct LocalDataPurgeInput {
1275 pub purge_id: String,
1276 pub targets: Vec<LocalDataPurgeTarget>,
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1281#[serde(rename_all = "camelCase")]
1282pub struct LocalDataPurgeResult {
1283 pub already_applied: bool,
1284 pub purged_rows: usize,
1285 pub dropped_commits: usize,
1286}
1287
1288#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1290#[serde(rename_all = "camelCase", deny_unknown_fields)]
1291pub struct LocalDataRebootstrapInput {
1292 pub rebootstrap_id: String,
1293}
1294
1295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1297#[serde(rename_all = "camelCase")]
1298pub struct LocalDataRebootstrapResult {
1299 pub already_applied: bool,
1300 pub retained_commits: usize,
1301 pub reset_subscriptions: usize,
1302}
1303
1304#[derive(Debug, Clone, Default)]
1306pub struct ClientLimits {
1307 pub limit_commits: Option<i32>,
1308 pub limit_snapshot_rows: Option<i32>,
1309 pub max_snapshot_pages: Option<i32>,
1310 pub accept: Option<u8>,
1313 pub blob_cache_max_bytes: Option<i64>,
1318 pub outcome_retention_max_entries: Option<usize>,
1321 pub max_push_commits_per_request: Option<usize>,
1323 pub max_push_operations_per_request: Option<usize>,
1328 pub max_push_request_bytes: Option<usize>,
1334 pub previous_version_context: Option<PreviousVersionContextConfig>,
1338 pub authority_reads: Vec<AuthorityReadDeclaration>,
1340}
1341
1342#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1344#[serde(rename_all = "camelCase", deny_unknown_fields)]
1345pub struct AuthorityReadDeclaration {
1346 pub table: String,
1347 pub columns: Vec<String>,
1348 pub scopes: std::collections::BTreeMap<String, Vec<String>>,
1349}