core: extract the store and sync engine into a shared crate (M12 step 1)
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

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:
2026-08-17 23:12:26 -04:00
co-authored by Claude Opus 5
parent c28f2bc00e
commit 0a7480cf9b
75 changed files with 246 additions and 1346 deletions
+283
View File
@@ -0,0 +1,283 @@
//! Tauri command surface for the local store — the offline mirror of the REST API
//! the frontend's repository seam calls. Each command locks the shared connection and
//! delegates to `store`; errors are stringified for the JS boundary. adapters/local.ts
//! (M10.5) invokes these.
use serde_json::Value;
use tauri::State;
use thoughtsync_core::local::models::*;
use thoughtsync_core::local::retention;
use thoughtsync_core::local::store;
use thoughtsync_core::local::Db;
use thoughtsync_core::sync::state;
// A macro would hide the (very regular) locking; kept explicit so each command reads
// as an obvious lock -> delegate -> stringify.
#[tauri::command]
pub fn config_get(db: State<'_, Db>) -> PublicConfig {
// What the Trash view counts down against: the linked server's window if we know
// it, else this device's own. Reading it here rather than hard-coding the offline
// default is what keeps the deadline on screen equal to the one that will actually
// be enforced. A store error falls back to the default rather than failing the
// call — the app must still boot.
let fallback = retention::LOCAL_RETENTION_DAYS;
let retention_days = match db.0.lock() {
Ok(conn) => state::effective_retention_days(&conn, fallback).unwrap_or(fallback),
Err(_) => fallback,
};
// Offline defaults: no signups, no server-side URL unfurling (needs network).
PublicConfig {
site_name: "ThoughtSync".to_string(),
allow_registration: false,
version: env!("CARGO_PKG_VERSION").to_string(),
enable_url_unfurl: false,
trash_retention_days: retention_days.max(0) as u32,
}
}
#[tauri::command]
pub fn auth_me() -> User {
// The single synthetic local user, so the auth-gated router resolves with no login.
User {
id: "local".to_string(),
email: "local@thoughtsync.app".to_string(),
display_name: "You".to_string(),
email_verified: true,
is_admin: false,
}
}
#[tauri::command]
pub fn notes_list(query: ListQuery, db: State<'_, Db>) -> Result<Vec<Note>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_notes(&conn, &query).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_get(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::get_note(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_create(input: NoteCreateInput, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_note(&conn, &input).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_create_titled(title: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_titled(&conn, &title).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_update(id: String, changes: Value, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::update_note(&conn, &id, &changes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_complete_reminder(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::complete_reminder(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_snooze_reminder(id: String, minutes: i64, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::snooze_reminder(&conn, &id, minutes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_set_labels(
id: String,
label_ids: Vec<String>,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::set_labels(&conn, &id, &label_ids).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_add_item(id: String, text: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::add_item(&conn, &id, &text).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_update_item(
id: String,
item_id: String,
changes: Value,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::update_item(&conn, &id, &item_id, &changes).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_item(id: String, item_id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_item(&conn, &id, &item_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_attachment(
id: String,
att_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_attachment(&conn, &id, &att_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_preview(
id: String,
preview_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_preview(&conn, &id, &preview_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_reorder(ordered_ids: Vec<String>, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::reorder(&conn, &ordered_ids).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_trash(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::trash(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_restore(id: String, db: State<'_, Db>) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::restore(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_delete_forever(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::delete_forever(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_revisions(id: String, db: State<'_, Db>) -> Result<Vec<NoteRevision>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::revisions(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_restore_revision(
id: String,
rev_id: String,
db: State<'_, Db>,
) -> Result<Note, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::restore_revision(&conn, &id, &rev_id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_reminders(db: State<'_, Db>) -> Result<Vec<Note>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::reminders(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_titles(db: State<'_, Db>) -> Result<Vec<TitleEntry>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::titles(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_search(q: String, db: State<'_, Db>) -> Result<Vec<Note>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::search(&conn, &q).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_backlinks(id: String, db: State<'_, Db>) -> Result<Vec<Backlink>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::backlinks(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn notes_link_search(q: String, db: State<'_, Db>) -> Result<Vec<TitleEntry>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::link_search(&conn, &q).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_list(db: State<'_, Db>) -> Result<Vec<Label>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_labels(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_create(name: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_label(&conn, &name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_rename(id: String, name: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::rename_label(&conn, &id, &name).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_set_color(id: String, color: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::set_label_color(&conn, &id, &color).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_remove(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::remove_label(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn labels_merge(source_id: String, into: String, db: State<'_, Db>) -> Result<Label, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::merge_labels(&conn, &source_id, &into).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_list(db: State<'_, Db>) -> Result<Vec<SavedFilter>, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::list_saved_filters(&conn).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_create(
name: String,
params: Value,
db: State<'_, Db>,
) -> Result<SavedFilter, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::create_saved_filter(&conn, &name, &params).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_remove(id: String, db: State<'_, Db>) -> Result<(), String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::remove_saved_filter(&conn, &id).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn saved_filters_rename(
id: String,
name: String,
db: State<'_, Db>,
) -> Result<SavedFilter, String> {
let conn = db.0.lock().map_err(|e| e.to_string())?;
store::rename_saved_filter(&conn, &id, &name).map_err(|e| e.to_string())
}