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 recommended_action: Option<String>,
833 },
834 RealtimeUnavailable {
839 state: RealtimeState,
840 reason_code: Option<String>,
841 retry_delay_ms: u64,
842 },
843 BudgetExhausted(SyncReport),
847}
848
849impl SyncOutcome {
850 pub fn to_json(&self) -> Value {
851 match self {
852 SyncOutcome::Ok(report) => {
853 let mut map = Map::new();
854 map.insert("ok".to_owned(), Value::Bool(true));
855 map.insert(
856 "report".to_owned(),
857 serde_json::to_value(report).expect("report serializes"),
858 );
859 Value::Object(map)
860 }
861 SyncOutcome::Failed {
862 error_code,
863 message,
864 details,
865 recommended_action,
866 } => {
867 let mut map = Map::new();
868 map.insert("ok".to_owned(), Value::Bool(false));
869 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
870 map.insert("message".to_owned(), Value::from(message.clone()));
871 if let Some(details) = details {
872 map.insert("details".to_owned(), details.clone());
873 }
874 if let Some(recommended_action) = recommended_action {
875 map.insert(
876 "recommendedAction".to_owned(),
877 Value::from(recommended_action.clone()),
878 );
879 }
880 Value::Object(map)
881 }
882 SyncOutcome::RealtimeUnavailable {
883 state,
884 reason_code,
885 retry_delay_ms,
886 } => {
887 let mut map = Map::new();
888 map.insert("ok".to_owned(), Value::Bool(false));
889 map.insert(
890 "errorCode".to_owned(),
891 Value::from(REALTIME_UNAVAILABLE_CODE),
892 );
893 map.insert(
894 "message".to_owned(),
895 Value::from("realtime is required and the realtime socket is not connected"),
896 );
897 map.insert(
898 "realtimeState".to_owned(),
899 serde_json::to_value(state).expect("state serializes"),
900 );
901 if let Some(reason_code) = reason_code {
902 map.insert(
903 "realtimeReasonCode".to_owned(),
904 Value::from(reason_code.clone()),
905 );
906 }
907 map.insert("retryDelayMs".to_owned(), Value::from(*retry_delay_ms));
908 Value::Object(map)
909 }
910 SyncOutcome::BudgetExhausted(report) => {
911 let mut map = Map::new();
912 map.insert("ok".to_owned(), Value::Bool(true));
913 map.insert("budgetExhausted".to_owned(), Value::Bool(true));
914 map.insert(
915 "report".to_owned(),
916 serde_json::to_value(report).expect("report serializes"),
917 );
918 Value::Object(map)
919 }
920 }
921 }
922}
923
924#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
925#[serde(rename_all = "camelCase")]
926pub struct ConflictRecord {
927 pub client_commit_id: String,
928 pub op_index: i32,
929 pub table: String,
930 pub row_id: String,
931 pub code: String,
932 pub message: String,
933 pub server_version: i64,
934 pub server_row: Map<String, Value>,
937 #[serde(default, skip_serializing_if = "Vec::is_empty")]
942 pub conflict_columns: Vec<String>,
943 #[serde(skip_serializing_if = "Option::is_none")]
944 pub operation: Option<CommitOperation>,
945}
946
947#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
950#[serde(rename_all = "camelCase", deny_unknown_fields)]
951pub struct RejectionDetails {
952 #[serde(skip_serializing_if = "Option::is_none")]
953 pub field_paths: Option<Vec<String>>,
954 #[serde(skip_serializing_if = "Option::is_none")]
955 pub reason: Option<String>,
956 #[serde(skip_serializing_if = "Option::is_none")]
957 pub required_action: Option<String>,
958 #[serde(skip_serializing_if = "Option::is_none")]
959 pub references: Option<BTreeMap<String, String>>,
960}
961
962impl RejectionDetails {
963 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
964 if raw.len() > 4_096 {
965 return Err("rejection details exceed 4096 encoded bytes".to_owned());
966 }
967 let details: Self = serde_json::from_str(raw)
968 .map_err(|error| format!("invalid rejection details: {error}"))?;
969 details.validate()?;
970 Ok(details)
971 }
972
973 fn validate(&self) -> Result<(), String> {
974 let mut members = 0;
975 if let Some(paths) = &self.field_paths {
976 members += 1;
977 if paths.is_empty() || paths.len() > 32 {
978 return Err("fieldPaths must contain 1-32 paths".to_owned());
979 }
980 let mut seen = std::collections::BTreeSet::new();
981 for path in paths {
982 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
983 {
984 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
985 }
986 }
987 }
988 if let Some(reason) = &self.reason {
989 members += 1;
990 if !valid_token(reason, 96) {
991 return Err("reason must be a lowercase stable token".to_owned());
992 }
993 }
994 if let Some(action) = &self.required_action {
995 members += 1;
996 if !valid_token(action, 96) {
997 return Err("requiredAction must be a lowercase stable token".to_owned());
998 }
999 }
1000 if let Some(references) = &self.references {
1001 members += 1;
1002 if references.is_empty() || references.len() > 16 {
1003 return Err("references must contain 1-16 entries".to_owned());
1004 }
1005 for (key, value) in references {
1006 if !valid_token(key, 64)
1007 || value.is_empty()
1008 || value.len() > 256
1009 || value.trim() != value
1010 || value.chars().any(char::is_control)
1011 {
1012 return Err("references contains an invalid key or value".to_owned());
1013 }
1014 }
1015 }
1016 if members == 0 {
1017 return Err("rejection details must not be empty".to_owned());
1018 }
1019 Ok(())
1020 }
1021}
1022
1023fn valid_identifier(segment: &str) -> bool {
1024 let mut chars = segment.chars();
1025 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
1026 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
1027}
1028
1029fn valid_token(token: &str, max: usize) -> bool {
1030 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
1031 {
1032 return false;
1033 }
1034 let mut previous_separator = false;
1035 for character in token.chars() {
1036 if character.is_ascii_lowercase() || character.is_ascii_digit() {
1037 previous_separator = false;
1038 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
1039 previous_separator = true;
1040 } else {
1041 return false;
1042 }
1043 }
1044 !previous_separator
1045}
1046
1047#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1048#[serde(rename_all = "camelCase")]
1049pub struct RejectionRecord {
1050 pub client_commit_id: String,
1051 pub op_index: i32,
1052 pub code: String,
1053 pub message: String,
1054 pub retryable: bool,
1055 #[serde(skip_serializing_if = "Option::is_none")]
1056 pub details: Option<RejectionDetails>,
1057 #[serde(skip_serializing_if = "Option::is_none")]
1058 pub operation: Option<CommitOperation>,
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1063#[serde(rename_all = "camelCase")]
1064pub struct CommitOperation {
1065 pub table: String,
1066 pub row_id: String,
1067 pub op: String,
1068 #[serde(skip_serializing_if = "Option::is_none")]
1069 pub base_version: Option<i64>,
1070 #[serde(skip_serializing_if = "Option::is_none")]
1071 pub values: Option<Map<String, Value>>,
1072}
1073
1074#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1075#[serde(rename_all = "snake_case")]
1076pub enum CommitOutcomeStatus {
1077 Applied,
1078 Cached,
1079 Conflict,
1080 Rejected,
1081}
1082
1083#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1084#[serde(rename_all = "snake_case")]
1085pub enum CommitOutcomeResolution {
1086 Active,
1087 ResolvedKeepServer,
1088 Superseded,
1089 Dismissed,
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1093#[serde(tag = "status", rename_all = "snake_case")]
1094pub enum CommitOperationOutcome {
1095 Applied {
1096 #[serde(rename = "opIndex")]
1097 op_index: i32,
1098 },
1099 Conflict {
1100 conflict: ConflictRecord,
1101 },
1102 Error {
1103 rejection: RejectionRecord,
1104 },
1105}
1106
1107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1108#[serde(rename_all = "camelCase")]
1109pub struct RetainedUniqueConflict {
1110 pub index: String,
1111 pub columns: Vec<String>,
1112 pub row_id: String,
1113 pub server_row: Map<String, Value>,
1114 pub server_version: i64,
1115}
1116
1117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1118#[serde(rename_all = "camelCase")]
1119pub struct RetainedCommitRow {
1120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1121 pub unique_conflicts: Vec<RetainedUniqueConflict>,
1122 pub table: String,
1123 pub row_id: String,
1124 pub local_row: Option<Map<String, Value>>,
1125 pub server_row: Option<Map<String, Value>>,
1126 pub server_version: Option<i64>,
1127}
1128
1129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1130#[serde(rename_all = "camelCase")]
1131pub struct CommitOutcome {
1132 pub sequence: i64,
1133 pub client_commit_id: String,
1134 pub status: CommitOutcomeStatus,
1135 pub recorded_at_ms: i64,
1136 pub results: Vec<CommitOperationOutcome>,
1137 #[serde(skip_serializing_if = "Option::is_none")]
1140 pub operations: Option<Vec<CommitOperation>>,
1141 #[serde(skip_serializing_if = "Option::is_none")]
1142 pub retained_rows: Option<Vec<RetainedCommitRow>>,
1143 pub resolution: CommitOutcomeResolution,
1144 #[serde(skip_serializing_if = "Option::is_none")]
1145 pub resolved_at_ms: Option<i64>,
1146 #[serde(skip_serializing_if = "Option::is_none")]
1147 pub replacement_client_commit_id: Option<String>,
1148}
1149
1150#[derive(Debug, Clone, Deserialize, Default)]
1151#[serde(rename_all = "camelCase")]
1152pub struct CommitOutcomeQuery {
1153 pub limit: Option<usize>,
1154 #[serde(default)]
1155 pub active_only: bool,
1156}
1157
1158#[derive(Debug, Clone, Serialize)]
1167#[serde(
1168 tag = "state",
1169 rename_all = "camelCase",
1170 rename_all_fields = "camelCase"
1171)]
1172pub enum SubscriptionCatchup {
1173 Unknown { id: String },
1175 Known {
1176 id: String,
1177 table: String,
1178 status: String,
1180 cursor: i64,
1181 has_resume_token: bool,
1182 bootstrap_complete: bool,
1186 known_pending_pages: bool,
1191 #[serde(skip_serializing_if = "Option::is_none")]
1192 effective_scopes: Option<Value>,
1193 #[serde(skip_serializing_if = "Option::is_none")]
1194 reason_code: Option<String>,
1195 },
1196}
1197
1198#[derive(Debug, Clone, Serialize)]
1207#[serde(tag = "status", rename_all = "snake_case")]
1208pub enum CommitDelivery {
1209 Pending {
1210 #[serde(rename = "clientCommitId")]
1211 client_commit_id: String,
1212 },
1213 Known {
1214 #[serde(rename = "clientCommitId")]
1215 client_commit_id: String,
1216 outcome: CommitOutcome,
1217 },
1218 Unknown {
1219 #[serde(rename = "clientCommitId")]
1220 client_commit_id: String,
1221 },
1222}
1223
1224#[derive(Debug, Clone, Serialize)]
1229#[serde(rename_all = "camelCase")]
1230pub struct SnapshotRead {
1231 pub revision: String,
1232 pub queries: Vec<Vec<QueryRow>>,
1233 pub coverage: CoverageSnapshot,
1234 pub subscriptions: Vec<SubscriptionCatchup>,
1235 pub deliveries: Vec<CommitDelivery>,
1236}
1237
1238#[derive(Debug, Clone, Deserialize)]
1239#[serde(rename_all = "camelCase")]
1240pub struct ResolveCommitOutcomeInput {
1241 pub client_commit_id: String,
1242 pub resolution: CommitOutcomeResolution,
1243 pub replacement_client_commit_id: Option<String>,
1244}
1245
1246#[derive(Debug, Clone, Serialize)]
1247#[serde(rename_all = "camelCase")]
1248pub struct RowState {
1249 pub row_id: String,
1250 pub version: i64,
1253 pub values: Map<String, Value>,
1254}
1255
1256#[derive(Debug, Clone, Serialize)]
1257#[serde(rename_all = "camelCase")]
1258pub struct SubscriptionStateView {
1259 pub id: String,
1260 pub table: String,
1261 pub status: String,
1263 pub cursor: i64,
1264 pub has_resume_token: bool,
1265 #[serde(skip_serializing_if = "Option::is_none")]
1266 pub effective_scopes: Option<Value>,
1267 #[serde(skip_serializing_if = "Option::is_none")]
1268 pub reason_code: Option<String>,
1269}
1270
1271#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1275#[serde(rename_all = "camelCase", deny_unknown_fields)]
1276pub struct LocalDataPurgeTarget {
1277 pub table: String,
1278 pub selectors: BTreeMap<String, Vec<String>>,
1279}
1280
1281#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1284#[serde(rename_all = "camelCase", deny_unknown_fields)]
1285pub struct LocalDataPurgeInput {
1286 pub purge_id: String,
1287 pub targets: Vec<LocalDataPurgeTarget>,
1288}
1289
1290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1292#[serde(rename_all = "camelCase")]
1293pub struct LocalDataPurgeResult {
1294 pub already_applied: bool,
1295 pub purged_rows: usize,
1296 pub dropped_commits: usize,
1297}
1298
1299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1301#[serde(rename_all = "camelCase", deny_unknown_fields)]
1302pub struct LocalDataRebootstrapInput {
1303 pub rebootstrap_id: String,
1304}
1305
1306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1308#[serde(rename_all = "camelCase")]
1309pub struct LocalDataRebootstrapResult {
1310 pub already_applied: bool,
1311 pub retained_commits: usize,
1312 pub reset_subscriptions: usize,
1313}
1314
1315#[derive(Debug, Clone, Default)]
1317pub struct ClientLimits {
1318 pub limit_commits: Option<i32>,
1319 pub limit_snapshot_rows: Option<i32>,
1320 pub max_snapshot_pages: Option<i32>,
1321 pub accept: Option<u8>,
1324 pub blob_cache_max_bytes: Option<i64>,
1329 pub outcome_retention_max_entries: Option<usize>,
1332 pub max_push_commits_per_request: Option<usize>,
1334 pub max_push_operations_per_request: Option<usize>,
1339 pub max_push_request_bytes: Option<usize>,
1345 pub previous_version_context: Option<PreviousVersionContextConfig>,
1349 pub authority_reads: Vec<AuthorityReadDeclaration>,
1351}
1352
1353#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1355#[serde(rename_all = "camelCase", deny_unknown_fields)]
1356pub struct AuthorityReadDeclaration {
1357 pub table: String,
1358 pub columns: Vec<String>,
1359 pub scopes: std::collections::BTreeMap<String, Vec<String>>,
1360}