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 AppliedServerCommit {
448 pub subscription_id: String,
449 pub table: String,
450 pub commit_seq: i64,
451 pub source: AppliedServerCommitSource,
452}
453
454#[derive(Debug, Clone, Copy, Serialize)]
455#[serde(rename_all = "camelCase")]
456pub enum AppliedServerCommitSource {
457 Delta,
458 Pull,
459}
460
461#[derive(Debug, Clone, Serialize)]
462#[serde(rename_all = "camelCase")]
463pub struct ClientChangeBatch {
464 pub revision: String,
465 pub tables: Vec<TableChange>,
466 pub windows: Vec<WindowChange>,
467 #[serde(skip_serializing_if = "Option::is_none")]
468 pub status: Option<SyncStatusSnapshot>,
469 pub conflicts_changed: bool,
470 pub rejections_changed: bool,
471 pub outcomes_changed: bool,
472 #[serde(skip_serializing_if = "Vec::is_empty")]
473 pub applied_commits: Vec<AppliedServerCommit>,
474}
475
476#[derive(Debug, Clone, Serialize)]
477#[serde(tag = "kind", rename_all = "camelCase")]
478pub enum SyncIntent {
479 None,
480 Interactive,
481 Background {
482 #[serde(rename = "delayMs")]
483 delay_ms: u64,
484 },
485}
486
487#[derive(Debug, Clone, Serialize)]
488pub struct CommandEffects {
489 pub sync: SyncIntent,
490}
491
492impl CommandEffects {
493 #[must_use]
494 pub fn none() -> Self {
495 Self {
496 sync: SyncIntent::None,
497 }
498 }
499
500 #[must_use]
501 pub fn interactive() -> Self {
502 Self {
503 sync: SyncIntent::Interactive,
504 }
505 }
506}
507
508#[derive(Debug, Clone)]
509pub struct WindowCoverage {
510 pub base: WindowBase,
511 pub units: Vec<String>,
512}
513
514#[derive(Debug, Clone, Serialize)]
515#[serde(rename_all = "camelCase")]
516pub struct WindowUnitRef {
517 pub base_key: String,
518 pub unit: String,
519}
520
521#[derive(Debug, Clone, Serialize)]
522#[serde(rename_all = "camelCase")]
523pub struct CoverageSnapshot {
524 pub complete: bool,
525 pub pending: Vec<WindowUnitRef>,
526 pub missing: Vec<WindowUnitRef>,
527}
528
529#[derive(Debug, Clone, Copy)]
532pub struct QueryOwner<'a> {
533 pub id: &'a str,
534 pub tables: &'a [&'a str],
535}
536
537#[derive(Debug, Clone, PartialEq, Eq)]
542pub struct QueryReadFailure {
543 pub code: Option<&'static str>,
544 pub sqlite_code: Option<i32>,
545 pub sqlite_message: Option<String>,
546 pub rollback_failure: Option<Box<QueryReadFailure>>,
547 pub message: String,
548}
549
550impl std::fmt::Display for QueryReadFailure {
551 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
552 match self.code {
553 Some(code) => write!(f, "{code}: {}", self.message),
554 None => f.write_str(&self.message),
555 }
556 }
557}
558
559impl From<String> for QueryReadFailure {
560 fn from(message: String) -> Self {
561 Self {
562 code: None,
563 sqlite_code: None,
564 sqlite_message: None,
565 rollback_failure: None,
566 message,
567 }
568 }
569}
570
571impl From<rusqlite::Error> for QueryReadFailure {
572 fn from(error: rusqlite::Error) -> Self {
574 let sqlite_code = match &error {
575 rusqlite::Error::SqliteFailure(failure, _) => Some(failure.extended_code),
576 _ => None,
577 };
578 match sqlite_code.map(|code| code & 0xff) {
579 Some(11 | 26) => Self {
580 code: Some("client.storage_corrupt"),
581 sqlite_code,
582 sqlite_message: Some(error.to_string()),
583 rollback_failure: None,
584 message: "local SQLite storage is corrupt".to_owned(),
585 },
586 Some(13) => Self {
587 code: Some("client.storage_full"),
588 sqlite_code,
589 sqlite_message: Some(error.to_string()),
590 rollback_failure: None,
591 message: "local SQLite storage is full".to_owned(),
592 },
593 Some(10) => Self {
594 code: Some("client.storage_io"),
595 sqlite_code,
596 sqlite_message: Some(error.to_string()),
597 rollback_failure: None,
598 message: "local SQLite storage I/O failed".to_owned(),
599 },
600 Some(5 | 6) => Self {
604 code: Some("client.storage_busy"),
605 sqlite_code,
606 sqlite_message: Some(error.to_string()),
607 rollback_failure: None,
608 message: "local SQLite storage is busy".to_owned(),
609 },
610 _ => Self {
611 code: None,
612 sqlite_code,
613 sqlite_message: Some(error.to_string()),
614 rollback_failure: None,
615 message: error.to_string(),
616 },
617 }
618 }
619}
620
621impl QueryReadFailure {
622 pub fn retryable(&self) -> bool {
625 self.code == Some("client.storage_busy")
626 }
627
628 pub fn details(&self) -> Option<Value> {
629 self.sqlite_code.map(|code| {
630 let mut details =
631 serde_json::json!({ "sqliteCode": code, "sqliteMessage": self.sqlite_message });
632 if let Some(rollback) = &self.rollback_failure {
633 details["rollbackFailure"] = serde_json::json!({
634 "sqliteCode": rollback.sqlite_code, "message": rollback.sqlite_message
635 });
636 }
637 details
638 })
639 }
640}
641
642#[derive(Debug, Clone, PartialEq, Eq)]
649pub struct ClientError {
650 pub code: String,
651 pub message: String,
652 pub details: Option<Value>,
653 pub retryable: bool,
654}
655
656const DEFAULT_CLIENT_ERROR_CODE: &str = "client.failed";
658
659const GENERIC_AUTHORING_MESSAGE: &str = "the local authoring operation failed";
661
662fn static_authoring_message(code: &str) -> Option<&'static str> {
665 match code {
666 "sync.unknown_table" => Some("the commit targets an unknown table"),
667 "sync.invalid_request" => Some("the authoring request is invalid"),
668 "sync.row_missing" => Some("a sparse patch requires a local row"),
669 "sync.constraint_violation" => Some("local write violates a unique constraint"),
670 DEFAULT_CLIENT_ERROR_CODE => Some(GENERIC_AUTHORING_MESSAGE),
671 _ => None,
672 }
673}
674
675fn is_client_code(candidate: &str) -> bool {
676 candidate.starts_with("client.") || candidate.starts_with("sync.")
677}
678
679impl std::fmt::Display for ClientError {
680 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
681 write!(f, "{}: {}", self.code, self.message)
682 }
683}
684
685impl std::error::Error for ClientError {}
686
687impl From<QueryReadFailure> for ClientError {
688 fn from(failure: QueryReadFailure) -> Self {
689 match failure.code {
690 Some(code) => {
692 let details = failure.details();
693 let retryable = failure.retryable();
694 Self {
695 code: code.to_owned(),
696 message: failure.message,
697 details,
698 retryable,
699 }
700 }
701 None => {
702 let mut details = failure.details().unwrap_or_else(|| serde_json::json!({}));
703 if let Some(object) = details.as_object_mut() {
704 object.insert("legacyCause".to_owned(), Value::String(failure.message));
705 }
706 Self {
707 code: DEFAULT_CLIENT_ERROR_CODE.to_owned(),
708 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
709 details: Some(details),
710 retryable: false,
711 }
712 }
713 }
714 }
715}
716
717impl From<String> for ClientError {
718 fn from(message: String) -> Self {
723 if !message.contains(": ") && is_client_code(&message) {
725 return Self {
726 code: message.clone(),
727 message: static_authoring_message(&message)
728 .unwrap_or(GENERIC_AUTHORING_MESSAGE)
729 .to_owned(),
730 details: None,
731 retryable: false,
732 };
733 }
734 let (code, rest) = match message.split_once(": ") {
735 Some((code, rest)) if is_client_code(code) => (code, Some(rest)),
736 _ => (DEFAULT_CLIENT_ERROR_CODE, None),
737 };
738 match static_authoring_message(code) {
739 Some(fixed) => Self {
740 code: code.to_owned(),
741 message: fixed.to_owned(),
742 details: (rest != Some(fixed))
743 .then(|| serde_json::json!({ "legacyCause": message })),
744 retryable: false,
745 },
746 None => Self {
747 code: code.to_owned(),
748 message: GENERIC_AUTHORING_MESSAGE.to_owned(),
749 details: Some(serde_json::json!({ "legacyCause": message })),
750 retryable: false,
751 },
752 }
753 }
754}
755
756#[derive(Debug, Clone, Serialize)]
757#[serde(rename_all = "camelCase")]
758pub struct QuerySnapshot {
759 pub revision: String,
760 pub rows: Vec<QueryRow>,
761 pub coverage: CoverageSnapshot,
762}
763
764impl WindowState {
765 #[must_use]
767 pub fn complete(&self, unit: &str) -> bool {
768 self.units.iter().any(|u| u == unit) && !self.pending.iter().any(|u| u == unit)
769 }
770}
771
772#[derive(Debug, Clone)]
774pub enum Mutation {
775 Patch {
776 table: String,
777 values: Map<String, Value>,
778 base_version: Option<i64>,
779 },
780 Upsert {
781 table: String,
782 values: Map<String, Value>,
783 base_version: Option<i64>,
784 },
785 Delete {
786 table: String,
787 row_id: String,
788 base_version: Option<i64>,
789 },
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
793#[serde(rename_all = "camelCase")]
794pub struct SchemaFloor {
795 #[serde(skip_serializing_if = "Option::is_none")]
796 pub required_schema_version: Option<i32>,
797 #[serde(skip_serializing_if = "Option::is_none")]
798 pub latest_schema_version: Option<i32>,
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
805#[serde(rename_all = "camelCase")]
806pub struct LeaseState {
807 #[serde(skip_serializing_if = "Option::is_none")]
808 pub lease_id: Option<String>,
809 #[serde(skip_serializing_if = "Option::is_none")]
810 pub expires_at_ms: Option<i64>,
811 #[serde(skip_serializing_if = "Option::is_none")]
812 pub error_code: Option<String>,
813}
814
815#[derive(Debug, Clone, Serialize)]
817#[serde(rename_all = "camelCase")]
818pub struct PresencePeer {
819 pub actor_id: String,
820 pub client_id: String,
821 pub doc: serde_json::Value,
822}
823
824#[derive(Debug, Clone, Serialize, Default)]
825#[serde(rename_all = "camelCase")]
826pub struct SyncReport {
827 pub pushed: u32,
828 pub applied: Vec<String>,
829 pub rejected: Vec<String>,
830 pub retryable: Vec<String>,
831 pub conflicts: u32,
832 pub commits_applied: u32,
833 pub segment_rows_applied: u32,
834 pub bootstrapping: Vec<String>,
835 pub resets: Vec<String>,
836 pub revoked: Vec<String>,
837 pub failed: Vec<String>,
838 pub deferred_commits: usize,
839 #[serde(skip_serializing_if = "Option::is_none")]
840 pub schema_floor: Option<SchemaFloor>,
841}
842
843impl SyncReport {
844 pub fn merge(&mut self, report: &Self) {
849 self.pushed += report.pushed;
850 self.applied.extend(report.applied.iter().cloned());
851 self.rejected.extend(report.rejected.iter().cloned());
852 self.retryable.extend(report.retryable.iter().cloned());
853 self.conflicts += report.conflicts;
854 self.commits_applied += report.commits_applied;
855 self.segment_rows_applied += report.segment_rows_applied;
856 self.bootstrapping = report.bootstrapping.clone();
857 self.resets.extend(report.resets.iter().cloned());
858 self.revoked.extend(report.revoked.iter().cloned());
859 self.failed.extend(report.failed.iter().cloned());
860 self.deferred_commits = report.deferred_commits;
861 self.schema_floor = report.schema_floor.clone();
862 }
863}
864
865#[derive(Debug, Clone)]
868pub enum SyncOutcome {
869 Ok(SyncReport),
870 Failed {
871 error_code: String,
872 message: String,
873 details: Option<Value>,
874 recommended_action: Option<String>,
878 },
879 RealtimeUnavailable {
884 state: RealtimeState,
885 reason_code: Option<String>,
886 retry_delay_ms: u64,
887 },
888 BudgetExhausted(SyncReport),
892}
893
894impl SyncOutcome {
895 pub fn to_json(&self) -> Value {
896 match self {
897 SyncOutcome::Ok(report) => {
898 let mut map = Map::new();
899 map.insert("ok".to_owned(), Value::Bool(true));
900 map.insert(
901 "report".to_owned(),
902 serde_json::to_value(report).expect("report serializes"),
903 );
904 Value::Object(map)
905 }
906 SyncOutcome::Failed {
907 error_code,
908 message,
909 details,
910 recommended_action,
911 } => {
912 let mut map = Map::new();
913 map.insert("ok".to_owned(), Value::Bool(false));
914 map.insert("errorCode".to_owned(), Value::from(error_code.clone()));
915 map.insert("message".to_owned(), Value::from(message.clone()));
916 if let Some(details) = details {
917 map.insert("details".to_owned(), details.clone());
918 }
919 if let Some(recommended_action) = recommended_action {
920 map.insert(
921 "recommendedAction".to_owned(),
922 Value::from(recommended_action.clone()),
923 );
924 }
925 Value::Object(map)
926 }
927 SyncOutcome::RealtimeUnavailable {
928 state,
929 reason_code,
930 retry_delay_ms,
931 } => {
932 let mut map = Map::new();
933 map.insert("ok".to_owned(), Value::Bool(false));
934 map.insert(
935 "errorCode".to_owned(),
936 Value::from(REALTIME_UNAVAILABLE_CODE),
937 );
938 map.insert(
939 "message".to_owned(),
940 Value::from("realtime is required and the realtime socket is not connected"),
941 );
942 map.insert(
943 "realtimeState".to_owned(),
944 serde_json::to_value(state).expect("state serializes"),
945 );
946 if let Some(reason_code) = reason_code {
947 map.insert(
948 "realtimeReasonCode".to_owned(),
949 Value::from(reason_code.clone()),
950 );
951 }
952 map.insert("retryDelayMs".to_owned(), Value::from(*retry_delay_ms));
953 Value::Object(map)
954 }
955 SyncOutcome::BudgetExhausted(report) => {
956 let mut map = Map::new();
957 map.insert("ok".to_owned(), Value::Bool(true));
958 map.insert("budgetExhausted".to_owned(), Value::Bool(true));
959 map.insert(
960 "report".to_owned(),
961 serde_json::to_value(report).expect("report serializes"),
962 );
963 Value::Object(map)
964 }
965 }
966 }
967}
968
969#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
970#[serde(rename_all = "camelCase")]
971pub struct ConflictRecord {
972 pub client_commit_id: String,
973 pub op_index: i32,
974 pub table: String,
975 pub row_id: String,
976 pub code: String,
977 pub message: String,
978 pub server_version: i64,
979 pub server_row: Map<String, Value>,
982 #[serde(default, skip_serializing_if = "Vec::is_empty")]
987 pub conflict_columns: Vec<String>,
988 #[serde(skip_serializing_if = "Option::is_none")]
989 pub operation: Option<CommitOperation>,
990}
991
992#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
995#[serde(rename_all = "camelCase", deny_unknown_fields)]
996pub struct RejectionDetails {
997 #[serde(skip_serializing_if = "Option::is_none")]
998 pub field_paths: Option<Vec<String>>,
999 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub reason: Option<String>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub required_action: Option<String>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub references: Option<BTreeMap<String, String>>,
1005}
1006
1007impl RejectionDetails {
1008 pub(crate) fn parse(raw: &str) -> Result<Self, String> {
1009 if raw.len() > 4_096 {
1010 return Err("rejection details exceed 4096 encoded bytes".to_owned());
1011 }
1012 let details: Self = serde_json::from_str(raw)
1013 .map_err(|error| format!("invalid rejection details: {error}"))?;
1014 details.validate()?;
1015 Ok(details)
1016 }
1017
1018 fn validate(&self) -> Result<(), String> {
1019 let mut members = 0;
1020 if let Some(paths) = &self.field_paths {
1021 members += 1;
1022 if paths.is_empty() || paths.len() > 32 {
1023 return Err("fieldPaths must contain 1-32 paths".to_owned());
1024 }
1025 let mut seen = std::collections::BTreeSet::new();
1026 for path in paths {
1027 if path.len() > 160 || !path.split('.').all(valid_identifier) || !seen.insert(path)
1028 {
1029 return Err("fieldPaths contains an invalid or duplicate path".to_owned());
1030 }
1031 }
1032 }
1033 if let Some(reason) = &self.reason {
1034 members += 1;
1035 if !valid_token(reason, 96) {
1036 return Err("reason must be a lowercase stable token".to_owned());
1037 }
1038 }
1039 if let Some(action) = &self.required_action {
1040 members += 1;
1041 if !valid_token(action, 96) {
1042 return Err("requiredAction must be a lowercase stable token".to_owned());
1043 }
1044 }
1045 if let Some(references) = &self.references {
1046 members += 1;
1047 if references.is_empty() || references.len() > 16 {
1048 return Err("references must contain 1-16 entries".to_owned());
1049 }
1050 for (key, value) in references {
1051 if !valid_token(key, 64)
1052 || value.is_empty()
1053 || value.len() > 256
1054 || value.trim() != value
1055 || value.chars().any(char::is_control)
1056 {
1057 return Err("references contains an invalid key or value".to_owned());
1058 }
1059 }
1060 }
1061 if members == 0 {
1062 return Err("rejection details must not be empty".to_owned());
1063 }
1064 Ok(())
1065 }
1066}
1067
1068fn valid_identifier(segment: &str) -> bool {
1069 let mut chars = segment.chars();
1070 matches!(chars.next(), Some(first) if first == '_' || first.is_ascii_alphabetic())
1071 && chars.all(|character| character == '_' || character.is_ascii_alphanumeric())
1072}
1073
1074fn valid_token(token: &str, max: usize) -> bool {
1075 if token.is_empty() || token.len() > max || !token.starts_with(|c: char| c.is_ascii_lowercase())
1076 {
1077 return false;
1078 }
1079 let mut previous_separator = false;
1080 for character in token.chars() {
1081 if character.is_ascii_lowercase() || character.is_ascii_digit() {
1082 previous_separator = false;
1083 } else if matches!(character, '.' | '_' | '-') && !previous_separator {
1084 previous_separator = true;
1085 } else {
1086 return false;
1087 }
1088 }
1089 !previous_separator
1090}
1091
1092#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1093#[serde(rename_all = "camelCase")]
1094pub struct RejectionRecord {
1095 pub client_commit_id: String,
1096 pub op_index: i32,
1097 pub code: String,
1098 pub message: String,
1099 pub retryable: bool,
1100 #[serde(skip_serializing_if = "Option::is_none")]
1101 pub details: Option<RejectionDetails>,
1102 #[serde(skip_serializing_if = "Option::is_none")]
1103 pub operation: Option<CommitOperation>,
1104}
1105
1106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1108#[serde(rename_all = "camelCase")]
1109pub struct CommitOperation {
1110 pub table: String,
1111 pub row_id: String,
1112 pub op: String,
1113 #[serde(skip_serializing_if = "Option::is_none")]
1114 pub base_version: Option<i64>,
1115 #[serde(skip_serializing_if = "Option::is_none")]
1116 pub values: Option<Map<String, Value>>,
1117 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1119 pub insert: bool,
1120}
1121
1122#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1123#[serde(rename_all = "snake_case")]
1124pub enum CommitOutcomeStatus {
1125 Applied,
1126 Cached,
1127 Conflict,
1128 Rejected,
1129}
1130
1131#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1132#[serde(rename_all = "snake_case")]
1133pub enum CommitOutcomeResolution {
1134 Active,
1135 ResolvedKeepServer,
1136 Superseded,
1137 Dismissed,
1138}
1139
1140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1141#[serde(tag = "status", rename_all = "snake_case")]
1142pub enum CommitOperationOutcome {
1143 Applied {
1144 #[serde(rename = "opIndex")]
1145 op_index: i32,
1146 },
1147 Conflict {
1148 conflict: ConflictRecord,
1149 },
1150 Error {
1151 rejection: RejectionRecord,
1152 },
1153}
1154
1155#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1156#[serde(rename_all = "camelCase")]
1157pub struct RetainedUniqueConflict {
1158 pub index: String,
1159 pub columns: Vec<String>,
1160 pub row_id: String,
1161 pub server_row: Map<String, Value>,
1162 pub server_version: i64,
1163}
1164
1165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1166#[serde(rename_all = "camelCase")]
1167pub struct RetainedCommitRow {
1168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1169 pub unique_conflicts: Vec<RetainedUniqueConflict>,
1170 pub table: String,
1171 pub row_id: String,
1172 pub local_row: Option<Map<String, Value>>,
1173 pub server_row: Option<Map<String, Value>>,
1175 pub server_version: Option<i64>,
1176}
1177
1178#[derive(Debug, Clone, Serialize, PartialEq)]
1180#[serde(rename_all = "camelCase")]
1181pub struct CurrentBaseProof {
1182 pub proven: bool,
1183 #[serde(skip_serializing_if = "Option::is_none")]
1184 pub rows: Option<Vec<RetainedCommitRow>>,
1185 #[serde(skip_serializing_if = "Option::is_none")]
1186 pub reason: Option<CurrentBaseProofReason>,
1187}
1188
1189#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
1190#[serde(rename_all = "snake_case")]
1191pub enum CurrentBaseProofReason {
1192 Offline,
1193 SyncFailed,
1194 SchemaFloor,
1195 BudgetExhausted,
1196 SnapshotFailed,
1197 MissingCoverage,
1198 Revoked,
1199 OutcomeUnavailable,
1200}
1201
1202impl CurrentBaseProof {
1203 pub fn failure(reason: CurrentBaseProofReason) -> Self {
1204 Self {
1205 proven: false,
1206 rows: None,
1207 reason: Some(reason),
1208 }
1209 }
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1213#[serde(rename_all = "camelCase")]
1214pub struct CommitOutcome {
1215 pub sequence: i64,
1216 pub client_commit_id: String,
1217 pub status: CommitOutcomeStatus,
1218 pub recorded_at_ms: i64,
1219 pub results: Vec<CommitOperationOutcome>,
1220 #[serde(skip_serializing_if = "Option::is_none")]
1223 pub operations: Option<Vec<CommitOperation>>,
1224 #[serde(skip_serializing_if = "Option::is_none")]
1225 pub retained_rows: Option<Vec<RetainedCommitRow>>,
1226 pub resolution: CommitOutcomeResolution,
1227 #[serde(skip_serializing_if = "Option::is_none")]
1228 pub resolved_at_ms: Option<i64>,
1229 #[serde(skip_serializing_if = "Option::is_none")]
1230 pub replacement_client_commit_id: Option<String>,
1231}
1232
1233#[derive(Debug, Clone, Deserialize, Default)]
1234#[serde(rename_all = "camelCase")]
1235pub struct CommitOutcomeQuery {
1236 pub limit: Option<usize>,
1237 #[serde(default)]
1238 pub active_only: bool,
1239}
1240
1241#[derive(Debug, Clone, Serialize)]
1250#[serde(
1251 tag = "state",
1252 rename_all = "camelCase",
1253 rename_all_fields = "camelCase"
1254)]
1255pub enum SubscriptionCatchup {
1256 Unknown { id: String },
1258 Known {
1259 id: String,
1260 table: String,
1261 status: String,
1263 cursor: i64,
1264 has_resume_token: bool,
1265 bootstrap_complete: bool,
1269 known_pending_pages: bool,
1274 #[serde(skip_serializing_if = "Option::is_none")]
1275 effective_scopes: Option<Value>,
1276 #[serde(skip_serializing_if = "Option::is_none")]
1277 reason_code: Option<String>,
1278 },
1279}
1280
1281#[derive(Debug, Clone, Serialize)]
1290#[serde(tag = "status", rename_all = "snake_case")]
1291pub enum CommitDelivery {
1292 Pending {
1293 #[serde(rename = "clientCommitId")]
1294 client_commit_id: String,
1295 },
1296 Known {
1297 #[serde(rename = "clientCommitId")]
1298 client_commit_id: String,
1299 outcome: CommitOutcome,
1300 },
1301 Unknown {
1302 #[serde(rename = "clientCommitId")]
1303 client_commit_id: String,
1304 },
1305}
1306
1307#[derive(Debug, Clone, Serialize)]
1312#[serde(rename_all = "camelCase")]
1313pub struct SnapshotRead {
1314 pub revision: String,
1315 pub queries: Vec<Vec<QueryRow>>,
1316 pub coverage: CoverageSnapshot,
1317 pub subscriptions: Vec<SubscriptionCatchup>,
1318 pub deliveries: Vec<CommitDelivery>,
1319}
1320
1321#[derive(Debug, Clone, Deserialize)]
1322#[serde(rename_all = "camelCase")]
1323pub struct ResolveCommitOutcomeInput {
1324 pub client_commit_id: String,
1325 pub resolution: CommitOutcomeResolution,
1326 pub replacement_client_commit_id: Option<String>,
1327}
1328
1329#[derive(Debug, Clone, Serialize)]
1330#[serde(rename_all = "camelCase")]
1331pub struct RowState {
1332 pub row_id: String,
1333 pub version: i64,
1336 pub values: Map<String, Value>,
1337}
1338
1339#[derive(Debug, Clone, Serialize)]
1340#[serde(rename_all = "camelCase")]
1341pub struct SubscriptionStateView {
1342 pub id: String,
1343 pub table: String,
1344 pub status: String,
1346 pub cursor: i64,
1347 pub has_resume_token: bool,
1348 #[serde(skip_serializing_if = "Option::is_none")]
1349 pub effective_scopes: Option<Value>,
1350 #[serde(skip_serializing_if = "Option::is_none")]
1351 pub reason_code: Option<String>,
1352}
1353
1354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1358#[serde(rename_all = "camelCase", deny_unknown_fields)]
1359pub struct LocalDataPurgeTarget {
1360 pub table: String,
1361 pub selectors: BTreeMap<String, Vec<String>>,
1362}
1363
1364#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1367#[serde(rename_all = "camelCase", deny_unknown_fields)]
1368pub struct LocalDataPurgeInput {
1369 pub purge_id: String,
1370 pub targets: Vec<LocalDataPurgeTarget>,
1371}
1372
1373#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1375#[serde(rename_all = "camelCase")]
1376pub struct LocalDataPurgeResult {
1377 pub already_applied: bool,
1378 pub purged_rows: usize,
1379 pub dropped_commits: usize,
1380}
1381
1382#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1384#[serde(rename_all = "camelCase", deny_unknown_fields)]
1385pub struct LocalDataRebootstrapInput {
1386 pub rebootstrap_id: String,
1387}
1388
1389#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1391#[serde(rename_all = "camelCase")]
1392pub struct LocalDataRebootstrapResult {
1393 pub already_applied: bool,
1394 pub retained_commits: usize,
1395 pub reset_subscriptions: usize,
1396}
1397
1398#[derive(Debug, Clone, Default)]
1400pub struct ClientLimits {
1401 pub limit_commits: Option<i32>,
1402 pub limit_snapshot_rows: Option<i32>,
1403 pub max_snapshot_pages: Option<i32>,
1404 pub accept: Option<u8>,
1407 pub blob_cache_max_bytes: Option<i64>,
1412 pub outcome_retention_max_entries: Option<usize>,
1415 pub max_push_commits_per_request: Option<usize>,
1417 pub max_push_operations_per_request: Option<usize>,
1422 pub max_push_request_bytes: Option<usize>,
1428 pub previous_version_context: Option<PreviousVersionContextConfig>,
1432 pub authority_reads: Vec<AuthorityReadDeclaration>,
1434}
1435
1436#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
1438#[serde(rename_all = "camelCase", deny_unknown_fields)]
1439pub struct AuthorityReadDeclaration {
1440 pub table: String,
1441 pub columns: Vec<String>,
1442 pub scopes: std::collections::BTreeMap<String, Vec<String>>,
1443}