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>
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
//! 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()
|
||||
}
|
||||
Reference in New Issue
Block a user