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, Copy, Serialize, Deserialize, PartialEq, Eq)]
276#[serde(rename_all = "snake_case")]
277pub enum TransportCauseKind {
278 Network,
279 Connect,
280 Tls,
281 Io,
282 Request,
283 Protocol,
284 Timeout,
285 RoundDeadline,
286 Aborted,
287 Body,
288 Status,
289 Redirect,
290 Unknown,
291}
292
293#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
294#[serde(rename_all = "camelCase")]
295pub struct DiagnosticLastRound {
296 pub status: String,
297 pub started_at_ms: i64,
298 pub completed_at_ms: i64,
299 pub duration_ms: i64,
300 #[serde(skip_serializing_if = "Option::is_none")]
301 pub counters: Option<DiagnosticRoundCounters>,
302 #[serde(skip_serializing_if = "Option::is_none")]
303 pub error_code: Option<String>,
304 #[serde(skip_serializing_if = "Option::is_none")]
305 pub cause_kind: Option<TransportCauseKind>,
306 #[serde(skip_serializing_if = "Option::is_none")]
307 pub http_status: Option<u16>,
308}
309
310#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
311#[serde(rename_all = "camelCase")]
312pub struct DiagnosticLastChange {
313 pub revision: String,
314 pub recorded_at_ms: i64,
315 pub tables: Vec<String>,
316 pub windows: Vec<String>,
317 pub domains_truncated: bool,
318 pub status_changed: bool,
319 pub conflicts_changed: bool,
320 pub rejections_changed: bool,
321 pub outcomes_changed: bool,
322}
323
324#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
325#[serde(rename_all = "camelCase")]
326pub struct ClientDiagnosticsStorage {
327 pub status: String,
328 #[serde(skip_serializing_if = "Option::is_none")]
329 pub database_bytes_approx: Option<i64>,
330 #[serde(skip_serializing_if = "Option::is_none")]
331 pub pending_outbox_bytes_approx: Option<i64>,
332 #[serde(skip_serializing_if = "Option::is_none")]
333 pub retained_outcome_bytes_approx: Option<i64>,
334 #[serde(skip_serializing_if = "Option::is_none")]
335 pub retained_outcome_entries: Option<i64>,
336 #[serde(skip_serializing_if = "Option::is_none")]
337 pub blob_cache_bytes_approx: Option<i64>,
338 #[serde(skip_serializing_if = "Option::is_none")]
339 pub pressure_reason_code: Option<String>,
340}
341
342#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
344#[serde(rename_all = "camelCase")]
345pub struct DiagnosticQueryFailure {
346 pub id: String,
347 pub tables: Vec<String>,
349 pub code: String,
351 #[serde(skip_serializing_if = "Option::is_none")]
353 pub sqlite_code: Option<i32>,
354 pub at_ms: i64,
356}
357
358#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
359#[serde(rename_all = "camelCase")]
360pub struct ClientDiagnosticsSnapshot {
361 pub version: u8,
362 pub captured_at_ms: i64,
363 pub host: ClientDiagnosticsHost,
364 pub security_lifecycle: String,
365 pub schema: ClientDiagnosticsSchema,
366 pub replica: ClientDiagnosticsReplica,
367 pub lease: ClientDiagnosticsLease,
368 pub subscriptions: Vec<DiagnosticSubscription>,
369 pub subscriptions_truncated: bool,
370 #[serde(skip_serializing_if = "Option::is_none")]
371 pub last_round: Option<DiagnosticLastRound>,
372 #[serde(skip_serializing_if = "Option::is_none")]
373 pub last_change: Option<DiagnosticLastChange>,
374 pub storage: ClientDiagnosticsStorage,
375 pub query_failures: Vec<DiagnosticQueryFailure>,
376}
377
378#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
379#[serde(rename_all = "camelCase")]
380pub struct ClientDiagnosticsSchema {
381 pub current_version: i32,
382 pub upgrading: bool,
383 #[serde(skip_serializing_if = "Option::is_none")]
384 pub required_version: Option<i32>,
385 #[serde(skip_serializing_if = "Option::is_none")]
386 pub latest_version: Option<i32>,
387}
388
389#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
390#[serde(rename_all = "camelCase")]
391pub struct ClientDiagnosticsReplica {
392 pub local_revision: String,
393 pub sync_needed: bool,
394 pub pending_outbox: usize,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
399#[serde(rename_all = "lowercase")]
400pub enum RealtimePolicy {
401 Required,
402 #[default]
403 Optional,
404 Off,
405}
406
407impl RealtimePolicy {
408 #[must_use]
409 pub fn as_str(self) -> &'static str {
410 match self {
411 RealtimePolicy::Required => "required",
412 RealtimePolicy::Optional => "optional",
413 RealtimePolicy::Off => "off",
414 }
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
423#[serde(rename_all = "lowercase")]
424pub enum RealtimeState {
425 Connected,
426 Connecting,
427 #[default]
428 Disconnected,
429 Lost,
430 Refused,
431 Disabled,
432}
433
434#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
435#[serde(rename_all = "camelCase")]
436pub struct ClientDiagnosticsLease {
437 pub state: String,
438 #[serde(skip_serializing_if = "Option::is_none")]
439 pub expires_at_ms: Option<i64>,
440 #[serde(skip_serializing_if = "Option::is_none")]
441 pub error_code: Option<String>,
442}
443
444#[derive(Debug, Clone, Serialize)]
446#[serde(rename_all = "camelCase")]
447pub struct ClientChangeBatch {
448 pub revision: String,
449 pub tables: Vec<TableChange>,
450 pub windows: Vec<WindowChange>,
451 #[serde(skip_serializing_if = "Option::is_none")]
452 pub status: Option<SyncStatusSnapshot>,
453 pub conflicts_changed: bool,
454 pub rejections_changed: bool,
455 pub outcomes_changed: bool,
456}
457
458#[derive(Debug, Clone, Serialize)]
459#[serde(tag = "kind", rename_all = "camelCase")]
460pub enum SyncIntent {
461 None,
462 Interactive,
463 Background {
464 #[serde(rename = "delayMs")]
465 delay_ms: u64,
466 },
467}
468
469#[derive(Debug, Clone, Serialize)]
470pub struct CommandEffects {
471 pub sync: SyncIntent,
472}
473
474impl CommandEffects {
475 #[must_use]
476 pub fn none() -> Self {
477 Self {
478 sync: SyncIntent::None,
479 }
480 }
481
482 #[must_use]
483 pub fn interactive() -> Self {
484 Self {
485 sync: SyncIntent::Interactive,
486 }
487 }
488}
489
490#[derive(Debug, Clone)]
491pub struct WindowCoverage {
492 pub base: WindowBase,
493 pub units: Vec<String>,
494}
495
496#[derive(Debug, Clone, Serialize)]
497#[serde(rename_all = "camelCase")]
498pub struct WindowUnitRef {
499 pub base_key: String,
500 pub unit: String,
501}
502
503#[derive(Debug, Clone, Serialize)]
504#[serde(rename_all = "camelCase")]
505pub struct CoverageSnapshot {
506 pub complete: bool,
507 pub pending: Vec<WindowUnitRef>,
508 pub missing: Vec<WindowUnitRef>,
509}
510
511#[derive(Debug, Clone, Copy)]
514pub struct QueryOwner<'a> {
515 pub id: &'a str,
516 pub tables: &'a [&'a str],
517}
518
519#[derive(Debug, Clone, PartialEq, Eq)]
524pub struct QueryReadFailure {
525 pub code: Option<&'static str>,
526 pub sqlite_code: Option<i32>,
527 pub sqlite_message: Option<String>,
528 pub rollback_failure: Option<Box<QueryReadFailure>>,
529 pub message: String,
530}
531
532impl std::fmt::Display for QueryReadFailure {
533 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
534 match self.code {
535 Some(code) => write!(f, "{code}: {}", self.message),
536 None => f.write_str(&self.message),
537 }
538 }
539}
540
541impl From<String> for QueryReadFailure {
542 fn from(message: String) -> Self {
543 Self {
544 code: None,
545 sqlite_code: None,
546 sqlite_message: None,
547 rollback_failure: None,
548 message,
549 }
550 }
551}
552
553impl From<rusqlite::Error> for QueryReadFailure {
554 fn from(error: rusqlite::Error) -> Self {
556 let sqlite_code = match &error {
557 rusqlite::Error::SqliteFailure(failure, _) => Some(failure.extended_code),
558 _ => None,
559 };
560 match sqlite_code.map(|code| code & 0xff) {
561 Some(11 | 26) => Self {
562 code: Some("client.storage_corrupt"),
563 sqlite_code,
564 sqlite_message: Some(error.to_string()),
565 rollback_failure: None,
566 message: "local SQLite storage is corrupt".to_owned(),
567 },
568 Some(13) => Self {
569 code: Some("client.storage_full"),
570 sqlite_code,
571 sqlite_message: Some(error.to_string()),
572 rollback_failure: None,
573 message: "local SQLite storage is full".to_owned(),
574 },
575 Some(10) => Self {
576 code: Some("client.storage_io"),
577 sqlite_code,
578 sqlite_message: Some(error.to_string()),
579 rollback_failure: None,
580 message: "local SQLite storage I/O failed".to_owned(),
581 },
582 Some(5 | 6) => Self {
586 code: Some("client.storage_busy"),
587 sqlite_code,
588 sqlite_message: Some(error.to_string()),
589 rollback_failure: None,
590 message: "local SQLite storage is busy".to_owned(),
591 },
592 _ => Self {
593 code: None,
594 sqlite_code,
595 sqlite_message: Some(error.to_string()),
596 rollback_failure: None,
597 message: error.to_string(),
598 },
599 }
600 }
601}
602
603impl QueryReadFailure {
604 pub fn retryable(&self) -> bool {
607 self.code == Some("client.storage_busy")
608 }
609
610 pub fn details(&self) -> Option<Value> {
611 self.sqlite_code.map(|code| {
612 let mut details =
613 serde_json::json!({ "sqliteCode": code, "sqliteMessage": self.sqlite_message });
614 if let Some(rollback) = &self.rollback_failure {
615 details["rollbackFailure"] = serde_json::json!({
616 "sqliteCode": rollback.sqlite_code, "message": rollback.sqlite_message
617 });
618 }
619 details
620 })
621 }
622}
623
624#[derive(Debug, Clone, PartialEq, Eq)]
631pub struct ClientError {
632 pub code: String,
633 pub message: String,
634 pub details: Option<Value>,
635 pub retryable: bool,
636}
637
638const DEFAULT_CLIENT_ERROR_CODE: &str = "client.failed";
640
641const GENERIC_AUTHORING_MESSAGE: &str = "the local authoring operation failed";
643
644fn static_authoring_message(code: &str) -> Option<&'static str> {
647 match code {
648 "sync.unknown_table" => Some("the commit targets an unknown table"),
649 "sync.invalid_request" => Some("the authoring request is invalid"),
650 "sync.row_missing" => Some("a sparse patch requires a local row"),
651 "sync.constraint_violation" => Some("local write violates a unique constraint"),
652 DEFAULT_CLIENT_ERROR_CODE => Some(GENERIC_AUTHORING_MESSAGE),
653 _ => None,
654 }
655}
656
657fn is_client_code(candidate: &str) -> bool {
658 candidate.starts_with("client.") || candidate.starts_with("sync.")
659}
660
661impl std::fmt::Display for ClientError {
662 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
663 write!(f, "{}: {}", self.code, self.message)
664 }
665}
666
667impl std::error::Error for ClientError {}
668
669impl From<QueryReadFailure> for ClientError {
670 fn from(failure: QueryReadFailure) -> Self {
671 match failure.code {
672 Some(code) => {
674 let details = failure.details();
675 let retryable = failure.retryable();
676 Self {
677 code: code.to_owned(),
678 message: failure.message,
679 details,
680 retryable,
681 }
682 }
683 None => {
684 let mut details = failure.details().unwrap_or_else(|| serde_json::json!({}));
685 if let Some(object) = details.as_object_mut() {
686 object.insert("legacyCause".to_owned(), Value::String(failure.message));
687 }
688 Self {
689 code: DEFAULT_CLIENT_ERROR_CODE.to_owned(),
690 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
691 details: Some(details),
692 retryable: false,
693 }
694 }
695 }
696 }
697}
698
699impl From<String> for ClientError {
700 fn from(message: String) -> Self {
705 if !message.contains(": ") && is_client_code(&message) {
707 return Self {
708 code: message.clone(),
709 message: static_authoring_message(&message)
710 .unwrap_or(GENERIC_AUTHORING_MESSAGE)
711 .to_owned(),
712 details: None,
713 retryable: false,
714 };
715 }
716 let (code, rest) = match message.split_once(": ") {
717 Some((code, rest)) if is_client_code(code) => (code, Some(rest)),
718 _ => (DEFAULT_CLIENT_ERROR_CODE, None),
719 };
720 match static_authoring_message(code) {
721 Some(fixed) => Self {
722 code: code.to_owned(),
723 message: fixed.to_owned(),
724 details: (rest != Some(fixed))
725 .then(|| serde_json::json!({ "legacyCause": message })),
726 retryable: false,
727 },
728 None => Self {
729 code: code.to_owned(),
730 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
731 details: Some(serde_json::json!({ "legacyCause": message })),
732 retryable: false,
733 },
734 }
735 }
736}
737
738#[derive(Debug, Clone, Serialize)]
739#[serde(rename_all = "camelCase")]
740pub struct QuerySnapshot {
741 pub revision: String,
742 pub rows: Vec<QueryRow>,
743 pub coverage: CoverageSnapshot,
744}
745
746impl WindowState {
747 #[must_use]
749 pub fn complete(&self, unit: &str) -> bool {
750 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
751 }
752}
753
754#[derive(Debug, Clone)]
756pub enum Mutation {
757 Patch {
758 table: String,
759 values: Map<String, Value>,
760 base_version: Option<i64>,
761 },
762 Upsert {
763 table: String,
764 values: Map<String, Value>,
765 base_version: Option<i64>,
766 },
767 Delete {
768 table: String,
769 row_id: String,
770 base_version: Option<i64>,
771 },
772}
773
774#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
775#[serde(rename_all = "camelCase")]
776pub struct SchemaFloor {
777 #[serde(skip_serializing_if = "Option::is_none")]
778 pub required_schema_version: Option<i32>,
779 #[serde(skip_serializing_if = "Option::is_none")]
780 pub latest_schema_version: Option<i32>,
781}
782
783#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
787#[serde(rename_all = "camelCase")]
788pub struct LeaseState {
789 #[serde(skip_serializing_if = "Option::is_none")]
790 pub lease_id: Option<String>,
791 #[serde(skip_serializing_if = "Option::is_none")]
792 pub expires_at_ms: Option<i64>,
793 #[serde(skip_serializing_if = "Option::is_none")]
794 pub error_code: Option<String>,
795}
796
797#[derive(Debug, Clone, Serialize)]
799#[serde(rename_all = "camelCase")]
800pub struct PresencePeer {
801 pub actor_id: String,
802 pub client_id: String,
803 pub doc: serde_json::Value,
804}
805
806#[derive(Debug, Clone, Serialize, Default)]
807#[serde(rename_all = "camelCase")]
808pub struct SyncReport {
809 pub pushed: u32,
810 pub applied: Vec<String>,
811 pub rejected: Vec<String>,
812 pub retryable: Vec<String>,
813 pub conflicts: u32,
814 pub commits_applied: u32,
815 pub segment_rows_applied: u32,
816 pub bootstrapping: Vec<String>,
817 pub resets: Vec<String>,
818 pub revoked: Vec<String>,
819 pub failed: Vec<String>,
820 pub deferred_commits: usize,
821 #[serde(skip_serializing_if = "Option::is_none")]
822 pub schema_floor: Option<SchemaFloor>,
823}
824
825impl SyncReport {
826 pub fn merge(&mut self, report: &Self) {
831 self.pushed += report.pushed;
832 self.applied.extend(report.applied.iter().cloned());
833 self.rejected.extend(report.rejected.iter().cloned());
834 self.retryable.extend(report.retryable.iter().cloned());
835 self.conflicts += report.conflicts;
836 self.commits_applied += report.commits_applied;
837 self.segment_rows_applied += report.segment_rows_applied;
838 self.bootstrapping = report.bootstrapping.clone();
839 self.resets.extend(report.resets.iter().cloned());
840 self.revoked.extend(report.revoked.iter().cloned());
841 self.failed.extend(report.failed.iter().cloned());
842 self.deferred_commits = report.deferred_commits;
843 self.schema_floor = report.schema_floor.clone();
844 }
845}
846
847#[derive(Debug, Clone)]
850pub enum SyncOutcome {
851 Ok(SyncReport),
852 Failed {
853 error_code: String,
854 message: String,
855 details: Option<Value>,
856 recommended_action: Option<String>,
860 },
861 RealtimeUnavailable {
866 state: RealtimeState,
867 reason_code: Option<String>,
868 retry_delay_ms: u64,
869 },
870 BudgetExhausted(SyncReport),
874}
875
876impl SyncOutcome {
877 pub fn to_json(&self) -> Value {
878 match self {
879 SyncOutcome::Ok(report) => {
880 let mut map = Map::new();
881 map.insert("ok".to_owned(), Value::Bool(true));
882 map.insert(
883 "report".to_owned(),
884 serde_json::to_value(report).expect("report serializes"),
885 );
886 Value::Object(map)
887 }
888 SyncOutcome::Failed {
889 error_code,
890 message,
891 details,
892 recommended_action,
893 } => {
894 let mut map = Map::new();
895 map.insert("ok".to_owned(), Value::Bool(false));
896 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
897 map.insert("message".to_owned(), Value::from(message.clone()));
898 if let Some(details) = details {
899 map.insert("details".to_owned(), details.clone());
900 }
901 if let Some(recommended_action) = recommended_action {
902 map.insert(
903 "recommendedAction".to_owned(),
904 Value::from(recommended_action.clone()),
905 );
906 }
907 Value::Object(map)
908 }
909 SyncOutcome::RealtimeUnavailable {
910 state,
911 reason_code,
912 retry_delay_ms,
913 } => {
914 let mut map = Map::new();
915 map.insert("ok".to_owned(), Value::Bool(false));
916 map.insert(
917 "errorCode".to_owned(),
918 Value::from(REALTIME_UNAVAILABLE_CODE),
919 );
920 map.insert(
921 "message".to_owned(),
922 Value::from("realtime is required and the realtime socket is not connected"),
923 );
924 map.insert(
925 "realtimeState".to_owned(),
926 serde_json::to_value(state).expect("state serializes"),
927 );
928 if let Some(reason_code) = reason_code {
929 map.insert(
930 "realtimeReasonCode".to_owned(),
931 Value::from(reason_code.clone()),
932 );
933 }
934 map.insert("retryDelayMs".to_owned(), Value::from(*retry_delay_ms));
935 Value::Object(map)
936 }
937 SyncOutcome::BudgetExhausted(report) => {
938 let mut map = Map::new();
939 map.insert("ok".to_owned(), Value::Bool(true));
940 map.insert("budgetExhausted".to_owned(), Value::Bool(true));
941 map.insert(
942 "report".to_owned(),
943 serde_json::to_value(report).expect("report serializes"),
944 );
945 Value::Object(map)
946 }
947 }
948 }
949}
950
951#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
952#[serde(rename_all = "camelCase")]
953pub struct ConflictRecord {
954 pub client_commit_id: String,
955 pub op_index: i32,
956 pub table: String,
957 pub row_id: String,
958 pub code: String,
959 pub message: String,
960 pub server_version: i64,
961 pub server_row: Map<String, Value>,
964 #[serde(default, skip_serializing_if = "Vec::is_empty")]
969 pub conflict_columns: Vec<String>,
970 #[serde(skip_serializing_if = "Option::is_none")]
971 pub operation: Option<CommitOperation>,
972}
973
974#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
977#[serde(rename_all = "camelCase", deny_unknown_fields)]
978pub struct RejectionDetails {
979 #[serde(skip_serializing_if = "Option::is_none")]
980 pub field_paths: Option<Vec<String>>,
981 #[serde(skip_serializing_if = "Option::is_none")]
982 pub reason: Option<String>,
983 #[serde(skip_serializing_if = "Option::is_none")]
984 pub required_action: Option<String>,
985 #[serde(skip_serializing_if = "Option::is_none")]
986 pub references: Option<BTreeMap<String, String>>,
987}
988
989impl RejectionDetails {
990 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
991 if raw.len() > 4_096 {
992 return Err("rejection details exceed 4096 encoded bytes".to_owned());
993 }
994 let details: Self = serde_json::from_str(raw)
995 .map_err(|error| format!("invalid rejection details: {error}"))?;
996 details.validate()?;
997 Ok(details)
998 }
999
1000 fn validate(&self) -> Result<(), String> {
1001 let mut members = 0;
1002 if let Some(paths) = &self.field_paths {
1003 members += 1;
1004 if paths.is_empty() || paths.len() > 32 {
1005 return Err("fieldPaths must contain 1-32 paths".to_owned());
1006 }
1007 let mut seen = std::collections::BTreeSet::new();
1008 for path in paths {
1009 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
1010 {
1011 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
1012 }
1013 }
1014 }
1015 if let Some(reason) = &self.reason {
1016 members += 1;
1017 if !valid_token(reason, 96) {
1018 return Err("reason must be a lowercase stable token".to_owned());
1019 }
1020 }
1021 if let Some(action) = &self.required_action {
1022 members += 1;
1023 if !valid_token(action, 96) {
1024 return Err("requiredAction must be a lowercase stable token".to_owned());
1025 }
1026 }
1027 if let Some(references) = &self.references {
1028 members += 1;
1029 if references.is_empty() || references.len() > 16 {
1030 return Err("references must contain 1-16 entries".to_owned());
1031 }
1032 for (key, value) in references {
1033 if !valid_token(key, 64)
1034 || value.is_empty()
1035 || value.len() > 256
1036 || value.trim() != value
1037 || value.chars().any(char::is_control)
1038 {
1039 return Err("references contains an invalid key or value".to_owned());
1040 }
1041 }
1042 }
1043 if members == 0 {
1044 return Err("rejection details must not be empty".to_owned());
1045 }
1046 Ok(())
1047 }
1048}
1049
1050fn valid_identifier(segment: &str) -> bool {
1051 let mut chars = segment.chars();
1052 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
1053 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
1054}
1055
1056fn valid_token(token: &str, max: usize) -> bool {
1057 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
1058 {
1059 return false;
1060 }
1061 let mut previous_separator = false;
1062 for character in token.chars() {
1063 if character.is_ascii_lowercase() || character.is_ascii_digit() {
1064 previous_separator = false;
1065 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
1066 previous_separator = true;
1067 } else {
1068 return false;
1069 }
1070 }
1071 !previous_separator
1072}
1073
1074#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1075#[serde(rename_all = "camelCase")]
1076pub struct RejectionRecord {
1077 pub client_commit_id: String,
1078 pub op_index: i32,
1079 pub code: String,
1080 pub message: String,
1081 pub retryable: bool,
1082 #[serde(skip_serializing_if = "Option::is_none")]
1083 pub details: Option<RejectionDetails>,
1084 #[serde(skip_serializing_if = "Option::is_none")]
1085 pub operation: Option<CommitOperation>,
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1090#[serde(rename_all = "camelCase")]
1091pub struct CommitOperation {
1092 pub table: String,
1093 pub row_id: String,
1094 pub op: String,
1095 #[serde(skip_serializing_if = "Option::is_none")]
1096 pub base_version: Option<i64>,
1097 #[serde(skip_serializing_if = "Option::is_none")]
1098 pub values: Option<Map<String, Value>>,
1099 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1101 pub insert: bool,
1102}
1103
1104#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1105#[serde(rename_all = "snake_case")]
1106pub enum CommitOutcomeStatus {
1107 Applied,
1108 Cached,
1109 Conflict,
1110 Rejected,
1111}
1112
1113#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1114#[serde(rename_all = "snake_case")]
1115pub enum CommitOutcomeResolution {
1116 Active,
1117 ResolvedKeepServer,
1118 Superseded,
1119 Dismissed,
1120}
1121
1122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1123#[serde(tag = "status", rename_all = "snake_case")]
1124pub enum CommitOperationOutcome {
1125 Applied {
1126 #[serde(rename = "opIndex")]
1127 op_index: i32,
1128 },
1129 Conflict {
1130 conflict: ConflictRecord,
1131 },
1132 Error {
1133 rejection: RejectionRecord,
1134 },
1135}
1136
1137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1138#[serde(rename_all = "camelCase")]
1139pub struct RetainedUniqueConflict {
1140 pub index: String,
1141 pub columns: Vec<String>,
1142 pub row_id: String,
1143 pub server_row: Map<String, Value>,
1144 pub server_version: i64,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1148#[serde(rename_all = "camelCase")]
1149pub struct RetainedCommitRow {
1150 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1151 pub unique_conflicts: Vec<RetainedUniqueConflict>,
1152 pub table: String,
1153 pub row_id: String,
1154 pub local_row: Option<Map<String, Value>>,
1155 pub server_row: Option<Map<String, Value>>,
1157 pub server_version: Option<i64>,
1158}
1159
1160#[derive(Debug, Clone, Serialize, PartialEq)]
1162#[serde(rename_all = "camelCase")]
1163pub struct CurrentBaseProof {
1164 pub proven: bool,
1165 #[serde(skip_serializing_if = "Option::is_none")]
1166 pub rows: Option<Vec<RetainedCommitRow>>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1168 pub reason: Option<CurrentBaseProofReason>,
1169}
1170
1171#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
1172#[serde(rename_all = "snake_case")]
1173pub enum CurrentBaseProofReason {
1174 Offline,
1175 SyncFailed,
1176 SchemaFloor,
1177 BudgetExhausted,
1178 SnapshotFailed,
1179 MissingCoverage,
1180 Revoked,
1181 OutcomeUnavailable,
1182}
1183
1184impl CurrentBaseProof {
1185 pub fn failure(reason: CurrentBaseProofReason) -> Self {
1186 Self {
1187 proven: false,
1188 rows: None,
1189 reason: Some(reason),
1190 }
1191 }
1192}
1193
1194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1195#[serde(rename_all = "camelCase")]
1196pub struct CommitOutcome {
1197 pub sequence: i64,
1198 pub client_commit_id: String,
1199 pub status: CommitOutcomeStatus,
1200 pub recorded_at_ms: i64,
1201 pub results: Vec<CommitOperationOutcome>,
1202 #[serde(skip_serializing_if = "Option::is_none")]
1205 pub operations: Option<Vec<CommitOperation>>,
1206 #[serde(skip_serializing_if = "Option::is_none")]
1207 pub retained_rows: Option<Vec<RetainedCommitRow>>,
1208 pub resolution: CommitOutcomeResolution,
1209 #[serde(skip_serializing_if = "Option::is_none")]
1210 pub resolved_at_ms: Option<i64>,
1211 #[serde(skip_serializing_if = "Option::is_none")]
1212 pub replacement_client_commit_id: Option<String>,
1213}
1214
1215#[derive(Debug, Clone, Deserialize, Default)]
1216#[serde(rename_all = "camelCase")]
1217pub struct CommitOutcomeQuery {
1218 pub limit: Option<usize>,
1219 #[serde(default)]
1220 pub active_only: bool,
1221}
1222
1223#[derive(Debug, Clone, Serialize)]
1232#[serde(
1233 tag = "state",
1234 rename_all = "camelCase",
1235 rename_all_fields = "camelCase"
1236)]
1237pub enum SubscriptionCatchup {
1238 Unknown { id: String },
1240 Known {
1241 id: String,
1242 table: String,
1243 status: String,
1245 cursor: i64,
1246 has_resume_token: bool,
1247 bootstrap_complete: bool,
1251 known_pending_pages: bool,
1256 #[serde(skip_serializing_if = "Option::is_none")]
1257 effective_scopes: Option<Value>,
1258 #[serde(skip_serializing_if = "Option::is_none")]
1259 reason_code: Option<String>,
1260 },
1261}
1262
1263#[derive(Debug, Clone, Serialize)]
1272#[serde(tag = "status", rename_all = "snake_case")]
1273pub enum CommitDelivery {
1274 Pending {
1275 #[serde(rename = "clientCommitId")]
1276 client_commit_id: String,
1277 },
1278 Known {
1279 #[serde(rename = "clientCommitId")]
1280 client_commit_id: String,
1281 outcome: CommitOutcome,
1282 },
1283 Unknown {
1284 #[serde(rename = "clientCommitId")]
1285 client_commit_id: String,
1286 },
1287}
1288
1289#[derive(Debug, Clone, Serialize)]
1294#[serde(rename_all = "camelCase")]
1295pub struct SnapshotRead {
1296 pub revision: String,
1297 pub queries: Vec<Vec<QueryRow>>,
1298 pub coverage: CoverageSnapshot,
1299 pub subscriptions: Vec<SubscriptionCatchup>,
1300 pub deliveries: Vec<CommitDelivery>,
1301}
1302
1303#[derive(Debug, Clone, Deserialize)]
1304#[serde(rename_all = "camelCase")]
1305pub struct ResolveCommitOutcomeInput {
1306 pub client_commit_id: String,
1307 pub resolution: CommitOutcomeResolution,
1308 pub replacement_client_commit_id: Option<String>,
1309}
1310
1311#[derive(Debug, Clone, Serialize)]
1312#[serde(rename_all = "camelCase")]
1313pub struct RowState {
1314 pub row_id: String,
1315 pub version: i64,
1318 pub values: Map<String, Value>,
1319}
1320
1321#[derive(Debug, Clone, Serialize)]
1322#[serde(rename_all = "camelCase")]
1323pub struct SubscriptionStateView {
1324 pub id: String,
1325 pub table: String,
1326 pub status: String,
1328 pub cursor: i64,
1329 pub has_resume_token: bool,
1330 #[serde(skip_serializing_if = "Option::is_none")]
1331 pub effective_scopes: Option<Value>,
1332 #[serde(skip_serializing_if = "Option::is_none")]
1333 pub reason_code: Option<String>,
1334}
1335
1336#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1340#[serde(rename_all = "camelCase", deny_unknown_fields)]
1341pub struct LocalDataPurgeTarget {
1342 pub table: String,
1343 pub selectors: BTreeMap<String, Vec<String>>,
1344}
1345
1346#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1349#[serde(rename_all = "camelCase", deny_unknown_fields)]
1350pub struct LocalDataPurgeInput {
1351 pub purge_id: String,
1352 pub targets: Vec<LocalDataPurgeTarget>,
1353}
1354
1355#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1357#[serde(rename_all = "camelCase")]
1358pub struct LocalDataPurgeResult {
1359 pub already_applied: bool,
1360 pub purged_rows: usize,
1361 pub dropped_commits: usize,
1362}
1363
1364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1366#[serde(rename_all = "camelCase", deny_unknown_fields)]
1367pub struct LocalDataRebootstrapInput {
1368 pub rebootstrap_id: String,
1369}
1370
1371#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1373#[serde(rename_all = "camelCase")]
1374pub struct LocalDataRebootstrapResult {
1375 pub already_applied: bool,
1376 pub retained_commits: usize,
1377 pub reset_subscriptions: usize,
1378}
1379
1380#[derive(Debug, Clone, Default)]
1382pub struct ClientLimits {
1383 pub limit_commits: Option<i32>,
1384 pub limit_snapshot_rows: Option<i32>,
1385 pub max_snapshot_pages: Option<i32>,
1386 pub accept: Option<u8>,
1389 pub blob_cache_max_bytes: Option<i64>,
1394 pub outcome_retention_max_entries: Option<usize>,
1397 pub max_push_commits_per_request: Option<usize>,
1399 pub max_push_operations_per_request: Option<usize>,
1404 pub max_push_request_bytes: Option<usize>,
1410 pub previous_version_context: Option<PreviousVersionContextConfig>,
1414 pub authority_reads: Vec<AuthorityReadDeclaration>,
1416}
1417
1418#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1420#[serde(rename_all = "camelCase", deny_unknown_fields)]
1421pub struct AuthorityReadDeclaration {
1422 pub table: String,
1423 pub columns: Vec<String>,
1424 pub scopes: std::collections::BTreeMap<String, Vec<String>>,
1425}