Files
thoughtsync/core/src/sync/wire.rs
T
bvandeusenandClaude Opus 5 0a7480cf9b
Desktop (Tauri) / Tauri desktop (Linux) (push) Failing after 48s
Desktop (Tauri) / Windows installer (cross-compiled) (push) Successful in 1m50s
Desktop (Tauri) / Update manifest (push) Skipped
core: extract the store and sync engine into a shared crate (M12 step 1)
Android becomes a native Kotlin client over this same code (Scribe note 2730), so
the local store and sync engine stop being modules of the desktop app and become
`thoughtsync-core`, a crate with no UI framework in it at all.

This is a move, not a rewrite, and the measurement is why: every file in local/
and sync/ already carried ZERO Tauri references — 4,980 of 6,372 lines. The
coupling was 473 lines of command shim, which stays behind in the desktop crate
as src/commands/. Kept as git renames so history follows the files.

The desktop imports them under their old names (`use thoughtsync_core::{local,
sync}`) so every call site reads exactly as before. What moved is where they
live, not what they are.

Two things a workspace changes that are easy to miss, both caught before pushing:

[profile.release] now lives at the workspace ROOT. Cargo silently ignores
profiles declared by a non-root member — leaving it in the desktop crate would
have dropped lto/strip/opt-level from every release build with only a warning.

And a workspace shares ONE target dir, so the bundles moved from
desktop/src-tauri/target to target/. Thirteen references across publish-release,
debundle-graphics, verify.sh, package-prebuilt and the workflow now point there.
Pinning target-dir back would have been the smaller diff, but the Android lane
also produces Rust artifacts and they do not belong under desktop/.

Also retires the Tauri Android lane in the same push rather than leaving a path
that is being replaced: gen/android, android.yml and docs/android-dev.md are
gone, the mobile_entry_point attribute with them, and the lib drops to rlib —
staticlib/cdylib existed for Tauri mobile, and the .so Android loads will be
built from the core crate instead. Rule 22, no parallel path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:12:26 -04:00

167 lines
4.5 KiB
Rust

//! The delta-feed JSON shapes, exactly as `GET /api/sync/changes` sends them.
//!
//! Mirrors the server's serializers (`notes/serialize.py` + `serialize.py`) — see
//! `docs/sync.md` for the contract. Every field is `#[serde(default)]` or `Option`
//! so a NEWER server adding fields, or an older one omitting one, degrades to a
//! partial note rather than failing the whole page. Losing one attribute is
//! recoverable; refusing a page stalls sync permanently at that cursor.
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ChangesPage {
#[serde(default)]
pub notes: Vec<Note>,
#[serde(default)]
pub labels: Vec<Label>,
#[serde(default)]
pub cursor: i64,
#[serde(default)]
pub has_more: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Note {
pub id: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub body: String,
#[serde(default = "default_color")]
pub color: String,
#[serde(default = "default_kind")]
pub kind: String,
#[serde(default)]
pub position: i64,
#[serde(default)]
pub pinned: bool,
#[serde(default)]
pub archived: bool,
/// The server derives this from `deleted_at` — trash, NOT a tombstone.
#[serde(default)]
pub trashed: bool,
/// WHEN it was trashed. The trash-retention clock runs from here, so it has to be
/// the server's timestamp rather than anything this device invents. Absent from an
/// older server, which is why it's optional rather than required.
#[serde(default)]
pub deleted_at: Option<String>,
#[serde(default)]
pub remind_at: Option<String>,
#[serde(default)]
pub recurrence: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub updated_at: Option<String>,
#[serde(default)]
pub sync_revision: i64,
/// Set means the row was permanently purged: a content-less tombstone whose only
/// job is to tell clients to delete their copy.
#[serde(default)]
pub purged_at: Option<String>,
#[serde(default)]
pub labels: Vec<NoteLabel>,
#[serde(default)]
pub items: Vec<Item>,
#[serde(default)]
pub attachments: Vec<Attachment>,
#[serde(default)]
pub previews: Vec<Preview>,
}
impl Note {
pub fn is_tombstone(&self) -> bool {
self.purged_at.is_some()
}
}
/// A label as it appears attached to a note. Carries enough to materialize the label
/// row itself, which is what lets a membership be applied even if the label's own
/// delta hasn't arrived (see `pull::apply_page`).
#[derive(Debug, Clone, Deserialize)]
pub struct NoteLabel {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default = "default_color")]
pub color: String,
/// True when the membership came from a `#tag` in the body rather than a manual
/// assignment. Applied verbatim rather than re-derived — see `pull::apply_page`.
#[serde(default)]
pub via_tag: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Item {
pub id: String,
#[serde(default)]
pub text: String,
#[serde(default)]
pub checked: bool,
#[serde(default)]
pub position: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Attachment {
pub id: String,
#[serde(default)]
pub url: String,
#[serde(default)]
pub filename: Option<String>,
#[serde(default = "default_mime")]
pub mime: String,
#[serde(default)]
pub size: Option<i64>,
#[serde(default)]
pub sha256: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Preview {
pub id: String,
#[serde(default)]
pub url: String,
#[serde(default)]
pub title: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub image_url: Option<String>,
#[serde(default)]
pub site_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Label {
pub id: String,
#[serde(default)]
pub name: String,
#[serde(default = "default_color")]
pub color: String,
#[serde(default)]
pub sync_revision: i64,
#[serde(default)]
pub purged_at: Option<String>,
#[serde(default)]
pub created_at: Option<String>,
}
impl Label {
pub fn is_tombstone(&self) -> bool {
self.purged_at.is_some()
}
}
fn default_color() -> String {
"default".to_string()
}
fn default_kind() -> String {
"text".to_string()
}
fn default_mime() -> String {
"application/octet-stream".to_string()
}