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