max / synckit
11 files changed,
+77 insertions,
-6 deletions
| @@ -28,7 +28,9 @@ use crate::error::Result; | |||
| 28 | 28 | pub struct OtaRelease { | |
| 29 | 29 | /// Release id, used when registering artifacts. | |
| 30 | 30 | pub id: Uuid, | |
| 31 | + | /// Release version string (semver). | |
| 31 | 32 | pub version: String, | |
| 33 | + | /// Release notes / changelog text. | |
| 32 | 34 | pub notes: String, | |
| 33 | 35 | } | |
| 34 | 36 | ||
| @@ -47,10 +49,15 @@ pub struct OtaArtifactUpload { | |||
| 47 | 49 | /// updater plugin reads these five and nothing else. | |
| 48 | 50 | #[derive(Debug, Clone, Deserialize)] | |
| 49 | 51 | pub struct OtaManifest { | |
| 52 | + | /// Version offered by the update. | |
| 50 | 53 | pub version: String, | |
| 54 | + | /// Download URL for the update artifact. | |
| 51 | 55 | pub url: String, | |
| 56 | + | /// Detached signature Tauri verifies against the pinned public key. | |
| 52 | 57 | pub signature: String, | |
| 58 | + | /// Release notes shown in the update prompt. | |
| 53 | 59 | pub notes: String, | |
| 60 | + | /// Publish timestamp (RFC 3339). | |
| 54 | 61 | pub pub_date: String, | |
| 55 | 62 | } | |
| 56 | 63 |
| @@ -87,6 +87,7 @@ pub struct SubscriptionStatus { | |||
| 87 | 87 | #[derive(Debug, Clone, Serialize, Deserialize)] | |
| 88 | 88 | #[non_exhaustive] | |
| 89 | 89 | pub struct AppPricing { | |
| 90 | + | /// SyncKit app this pricing applies to. | |
| 90 | 91 | pub app_name: String, | |
| 91 | 92 | /// Minimum charge (applies to both monthly and annual). | |
| 92 | 93 | pub min_charge_cents: Cents, | |
| @@ -95,7 +96,9 @@ pub struct AppPricing { | |||
| 95 | 96 | pub per_gb_tenths_of_cent_per_month: i64, | |
| 96 | 97 | /// Annual price is monthly × this multiplier. | |
| 97 | 98 | pub annual_multiplier: i64, | |
| 99 | + | /// Smallest storage cap the slider allows, in bytes. | |
| 98 | 100 | pub min_cap_bytes: i64, | |
| 101 | + | /// Largest storage cap the slider allows, in bytes. | |
| 99 | 102 | pub max_cap_bytes: i64, | |
| 100 | 103 | } | |
| 101 | 104 | ||
| @@ -140,7 +143,9 @@ fn cap_bytes_to_gib_ceil(cap_bytes: i64) -> i64 { | |||
| 140 | 143 | /// break an older client's status parse. | |
| 141 | 144 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | |
| 142 | 145 | pub enum BillingInterval { | |
| 146 | + | /// Billed once per month. | |
| 143 | 147 | Monthly, | |
| 148 | + | /// Billed once per year. | |
| 144 | 149 | Annual, | |
| 145 | 150 | } | |
| 146 | 151 | ||
| @@ -158,6 +163,7 @@ impl<'de> Deserialize<'de> for BillingInterval { | |||
| 158 | 163 | } | |
| 159 | 164 | ||
| 160 | 165 | impl BillingInterval { | |
| 166 | + | /// The server wire string for this interval (`"monthly"` / `"annual"`). | |
| 161 | 167 | pub fn as_str(self) -> &'static str { | |
| 162 | 168 | match self { | |
| 163 | 169 | Self::Monthly => "monthly", | |
| @@ -200,11 +206,15 @@ pub struct AccountInfo { | |||
| 200 | 206 | pub username: String, | |
| 201 | 207 | } | |
| 202 | 208 | ||
| 209 | + | /// A single computed price point for a cap/interval pair. | |
| 203 | 210 | #[derive(Debug, Serialize, Deserialize)] | |
| 204 | 211 | #[non_exhaustive] | |
| 205 | 212 | pub struct PriceQuote { | |
| 213 | + | /// Storage cap this quote is for, in bytes. | |
| 206 | 214 | pub cap_bytes: i64, | |
| 215 | + | /// Billing interval this quote is for. | |
| 207 | 216 | pub interval: BillingInterval, | |
| 217 | + | /// Quoted price, in cents. | |
| 208 | 218 | pub price_cents: Cents, | |
| 209 | 219 | } | |
| 210 | 220 |
| @@ -142,6 +142,9 @@ pub enum Resolution { | |||
| 142 | 142 | /// Implement this to plug in app-specific merge logic. The `base` parameter | |
| 143 | 143 | /// is `Some` only if the app provides a base-store adapter. | |
| 144 | 144 | pub trait ConflictResolver: Send + Sync { | |
| 145 | + | /// Resolve a conflict between a `local` change and a `remote` change for the | |
| 146 | + | /// same row, optionally given the last-synced `base` value. Returns which | |
| 147 | + | /// side wins (or a merged result). | |
| 145 | 148 | fn resolve( | |
| 146 | 149 | &self, | |
| 147 | 150 | local: &ChangeEntry, |
| @@ -300,7 +300,12 @@ const WIRE_V2_TAG_BYTES: &[u8] = b"sk2:"; | |||
| 300 | 300 | #[derive(Clone, Copy, Debug)] | |
| 301 | 301 | pub enum AeadContext<'a> { | |
| 302 | 302 | /// A personal sync change entry, bound to its `(table, row_id)` address. | |
| 303 | - | Entry { table: &'a str, row_id: &'a str }, | |
| 303 | + | Entry { | |
| 304 | + | /// Table the row belongs to. | |
| 305 | + | table: &'a str, | |
| 306 | + | /// Row id within the table. | |
| 307 | + | row_id: &'a str, | |
| 308 | + | }, | |
| 304 | 309 | /// A group sync change entry, bound to its `(group_id, table, row_id)` | |
| 305 | 310 | /// address. Sealed under the group's GCK (see [`crate::identity`]), not the | |
| 306 | 311 | /// personal `master_key`. Folding `group_id` into the AAD makes a ciphertext | |
| @@ -308,12 +313,18 @@ pub enum AeadContext<'a> { | |||
| 308 | 313 | /// that moves a group-entry ciphertext into a different group's changelog | |
| 309 | 314 | /// changes its AAD, so it fails to open. | |
| 310 | 315 | GroupEntry { | |
| 316 | + | /// Group whose GCK sealed the entry. | |
| 311 | 317 | group_id: &'a str, | |
| 318 | + | /// Table the row belongs to. | |
| 312 | 319 | table: &'a str, | |
| 320 | + | /// Row id within the table. | |
| 313 | 321 | row_id: &'a str, | |
| 314 | 322 | }, | |
| 315 | 323 | /// A content-addressed blob, bound to its content hash. | |
| 316 | - | Blob { hash: &'a str }, | |
| 324 | + | Blob { | |
| 325 | + | /// Content hash the blob is addressed by. | |
| 326 | + | hash: &'a str, | |
| 327 | + | }, | |
| 317 | 328 | } | |
| 318 | 329 | ||
| 319 | 330 | impl<'a> AeadContext<'a> { |
| @@ -18,7 +18,9 @@ pub enum SyncKitError { | |||
| 18 | 18 | /// extracted from response. | |
| 19 | 19 | #[error("Server returned {status}: {message}")] | |
| 20 | 20 | Server { | |
| 21 | + | /// HTTP status code the server returned. | |
| 21 | 22 | status: u16, | |
| 23 | + | /// Error message extracted from the response. | |
| 22 | 24 | message: String, | |
| 23 | 25 | /// Parsed `Retry-After` header value in seconds (429 responses only). | |
| 24 | 26 | /// Hidden from public API, used internally by the retry loop. | |
| @@ -44,7 +46,12 @@ pub enum SyncKitError { | |||
| 44 | 46 | /// content address, the server served a different-but-authentic or | |
| 45 | 47 | /// rolled-back ciphertext under this hash. The bytes are discarded. | |
| 46 | 48 | #[error("Blob integrity check failed: expected {expected}, got {actual}")] | |
| 47 | - | IntegrityFailed { expected: String, actual: String }, | |
| 49 | + | IntegrityFailed { | |
| 50 | + | /// Content hash that was requested. | |
| 51 | + | expected: String, | |
| 52 | + | /// Content hash the downloaded bytes actually produced. | |
| 53 | + | actual: String, | |
| 54 | + | }, | |
| 48 | 55 | ||
| 49 | 56 | /// Key envelope JSON has an unrecognized version or missing fields. | |
| 50 | 57 | #[error("Invalid key envelope: {0}")] |
| @@ -66,6 +66,7 @@ | |||
| 66 | 66 | //! # Ok(()) | |
| 67 | 67 | //! # } | |
| 68 | 68 | //! ``` | |
| 69 | + | #![warn(missing_docs)] | |
| 69 | 70 | ||
| 70 | 71 | pub mod client; | |
| 71 | 72 | pub mod conflict; |
| @@ -26,8 +26,11 @@ use crate::error::{Result, SyncKitError}; | |||
| 26 | 26 | /// plaintext size in bytes. | |
| 27 | 27 | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 28 | 28 | pub struct BlobRef { | |
| 29 | + | /// SHA-256 content address (the blob's identity). | |
| 29 | 30 | pub hash: String, | |
| 31 | + | /// File extension, without the leading dot. | |
| 30 | 32 | pub ext: String, | |
| 33 | + | /// Plaintext size in bytes. | |
| 31 | 34 | pub size: u64, | |
| 32 | 35 | } | |
| 33 | 36 | ||
| @@ -44,8 +47,11 @@ pub trait BlobTransport: Sync { | |||
| 44 | 47 | hash: &str, | |
| 45 | 48 | path: &Path, | |
| 46 | 49 | ) -> impl Future<Output = Result<BlobUploadOutcome>> + Send; | |
| 50 | + | /// Confirm an uploaded blob so the server commits it at `hash`. | |
| 47 | 51 | fn confirm(&self, hash: &str, size: u64) -> impl Future<Output = Result<()>> + Send; | |
| 52 | + | /// Presigned download URL for the blob at `hash`. | |
| 48 | 53 | fn download_url(&self, hash: &str) -> impl Future<Output = Result<String>> + Send; | |
| 54 | + | /// Download the blob at `hash` from `url`, returning its ciphertext bytes. | |
| 49 | 55 | fn download(&self, hash: &str, url: &str) -> impl Future<Output = Result<Vec<u8>>> + Send; | |
| 50 | 56 | } | |
| 51 | 57 | ||
| @@ -96,7 +102,9 @@ pub trait BlobPolicy: Send + Sync { | |||
| 96 | 102 | /// Result of a blob sync pass. | |
| 97 | 103 | #[derive(Debug, Default, PartialEq, Eq)] | |
| 98 | 104 | pub struct BlobOutcome { | |
| 105 | + | /// Number of blobs uploaded this pass. | |
| 99 | 106 | pub uploaded: u64, | |
| 107 | + | /// Number of blobs downloaded this pass. | |
| 100 | 108 | pub downloaded: u64, | |
| 101 | 109 | } | |
| 102 | 110 |
| @@ -52,9 +52,13 @@ impl Default for SyncConfig { | |||
| 52 | 52 | /// Outcome of one [`SyncStore::sync_now`] cycle. | |
| 53 | 53 | #[derive(Debug, Default, PartialEq, Eq)] | |
| 54 | 54 | pub struct SyncOutcome { | |
| 55 | + | /// Number of local changes pushed to the server. | |
| 55 | 56 | pub pushed: u64, | |
| 57 | + | /// Number of remote changes pulled and applied. | |
| 56 | 58 | pub pulled: u64, | |
| 59 | + | /// Tables whose rows changed as a result of the pull. | |
| 57 | 60 | pub changed_tables: HashSet<String>, | |
| 61 | + | /// Blob upload/download tallies for this cycle. | |
| 58 | 62 | pub blobs: BlobOutcome, | |
| 59 | 63 | } | |
| 60 | 64 |
| @@ -33,7 +33,10 @@ pub enum SyncMode { | |||
| 33 | 33 | /// never replace. The export side emits a single UPDATE trigger, gated on one | |
| 34 | 34 | /// of the `set` columns actually changing. (Balanced Breakfast's `feed_items`: | |
| 35 | 35 | /// sync just `is_read`/`is_starred`, never the article body.) | |
| 36 | - | PartialUpdate { set: &'static [&'static str] }, | |
| 36 | + | PartialUpdate { | |
| 37 | + | /// Columns this table syncs; a remote change only ever `UPDATE`s these. | |
| 38 | + | set: &'static [&'static str], | |
| 39 | + | }, | |
| 37 | 40 | } | |
| 38 | 41 | ||
| 39 | 42 | /// How a table handles a remote delete. | |
| @@ -50,7 +53,11 @@ pub enum DeleteMode { | |||
| 50 | 53 | /// state the remote delete never intended to touch (audiofiles' `samples`). | |
| 51 | 54 | /// The export DELETE trigger is still generated normally, a *local* delete | |
| 52 | 55 | /// must still propagate; only the *apply* of a remote delete tombstones. | |
| 53 | - | Tombstone { column: &'static str }, | |
| 56 | + | Tombstone { | |
| 57 | + | /// Column set to `now()` in place of a hard delete when applying a | |
| 58 | + | /// remote delete. | |
| 59 | + | column: &'static str, | |
| 60 | + | }, | |
| 54 | 61 | } | |
| 55 | 62 | ||
| 56 | 63 | /// How a row's wire identifier (`sync_changelog.row_id`) is derived. |
| @@ -39,7 +39,12 @@ pub enum SyncScope { | |||
| 39 | 39 | /// The user's personal data, encrypted under the master key. | |
| 40 | 40 | Personal, | |
| 41 | 41 | /// A group's shared data, encrypted under the group's GCK at `gck_version`. | |
| 42 | - | Group { id: GroupId, gck_version: i32 }, | |
| 42 | + | Group { | |
| 43 | + | /// Group whose shared data this scope covers. | |
| 44 | + | id: GroupId, | |
| 45 | + | /// GCK version the group's data is encrypted under. | |
| 46 | + | gck_version: i32, | |
| 47 | + | }, | |
| 43 | 48 | } | |
| 44 | 49 | ||
| 45 | 50 | impl SyncScope { | |
| @@ -243,7 +248,9 @@ pub async fn ensure_device_registered<T: SyncTransport>( | |||
| 243 | 248 | /// Outcome of a pull pass. | |
| 244 | 249 | #[derive(Debug, Default, PartialEq, Eq)] | |
| 245 | 250 | pub struct PullOutcome { | |
| 251 | + | /// Number of remote changes applied. | |
| 246 | 252 | pub applied: u64, | |
| 253 | + | /// Tables whose rows changed as a result of the pull. | |
| 247 | 254 | pub changed_tables: HashSet<String>, | |
| 248 | 255 | } | |
| 249 | 256 |
| @@ -16,8 +16,11 @@ use uuid::Uuid; | |||
| 16 | 16 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] | |
| 17 | 17 | #[serde(rename_all = "UPPERCASE")] | |
| 18 | 18 | pub enum ChangeOp { | |
| 19 | + | /// A row was inserted. | |
| 19 | 20 | Insert, | |
| 21 | + | /// A row was updated. | |
| 20 | 22 | Update, | |
| 23 | + | /// A row was deleted. | |
| 21 | 24 | Delete, | |
| 22 | 25 | } | |
| 23 | 26 | ||
| @@ -223,6 +226,7 @@ pub(crate) struct RegisterDeviceRequest { | |||
| 223 | 226 | pub platform: String, | |
| 224 | 227 | } | |
| 225 | 228 | ||
| 229 | + | /// A registered device belonging to a user, as returned by the server. | |
| 226 | 230 | #[derive(Debug, Clone, Deserialize, Serialize)] | |
| 227 | 231 | #[non_exhaustive] | |
| 228 | 232 | pub struct Device { | |
| @@ -572,6 +576,7 @@ pub(crate) struct OAuthTokenResponse { | |||
| 572 | 576 | ||
| 573 | 577 | // ── Status ── | |
| 574 | 578 | ||
| 579 | + | /// Server-side sync status for the authenticated app/user. | |
| 575 | 580 | #[derive(Debug, Deserialize)] | |
| 576 | 581 | #[non_exhaustive] | |
| 577 | 582 | pub struct SyncStatus { | |
| @@ -589,6 +594,7 @@ pub(crate) struct BlobUploadUrlRequest { | |||
| 589 | 594 | pub size_bytes: i64, | |
| 590 | 595 | } | |
| 591 | 596 | ||
| 597 | + | /// Server response to a blob upload-URL request. | |
| 592 | 598 | #[derive(Deserialize)] | |
| 593 | 599 | #[non_exhaustive] | |
| 594 | 600 | pub struct BlobUploadUrlResponse { |