mirror of
https://github.com/priyanshujain/margin.git
synced 2026-10-04 03:57:03 +00:00
some more fixes
This commit is contained in:
1 parent
7a3c04f170
commit
2b86a407ba
63 files changed
+12172
-44
No files matched your search
@@ -0,0 +1,468 @@
|
||||
# Rust crates
|
||||
|
||||
The plumbing under the four apps that is genuinely the same code: the Tauri builder, the menu, the
|
||||
window lifecycle, the updater wiring, a log file, and a thin layer over rusqlite. Three crates, not
|
||||
ten. Every number below was measured on the four trees on 2026-09-06 and is recheckable against
|
||||
[.research/rust-core.md](.research/rust-core.md). `margin-google`, `margin-secrets` and `margin-http`
|
||||
belong to [accounts.md](accounts.md), `margin-typeset` and `margin-mac` to
|
||||
[typesetting.md](typesetting.md); they appear here only where a dependency between crates matters.
|
||||
|
||||
## Where this departs from repo-layout.md
|
||||
|
||||
[repo-layout.md](repo-layout.md) named its crates before this audit existed, and where the two
|
||||
disagree this document is the authority. Five changes, all narrowings except the last. `margin-db`
|
||||
becomes **`margin-sqlite`** and drops the FTS5 helpers, because FTS5 is in two apps and everything
|
||||
above the one shared tokenizer line differs: Docs ranks with `bm25` and `highlight`
|
||||
(`index.rs:1184-1188`), Mail uses the index as a membership subquery (`mirror/read.rs:234`) under a
|
||||
query language with `from:` and `has:` operators. **`margin-paths`** is not built: the five lines worth
|
||||
sharing are `app_data_dir`, which moves into `margin-shell`, and the rest, atomic write, trash and
|
||||
path validation, is three different algorithms with the divergence justified in a comment in each.
|
||||
**`margin-ipc`** is not built: all 165 commands already return the same shape, so there is no error
|
||||
type to unify. **`margin-update`** is a module inside `margin-shell` rather than a crate, because
|
||||
Margin's `updates.rs` is 90 lines and has no consumer that does not also want the builder prologue
|
||||
next to it. And **`margin-shell`** is new, has no entry in repo-layout.md, and is the largest single
|
||||
win in the audit.
|
||||
|
||||
## 1. The headline
|
||||
|
||||
73 distinct lines appear verbatim, indentation included, in all four apps' `src-tauri/src/lib.rs`.
|
||||
|
||||
| | `lib.rs` lines | code lines | verbatim in all four | share of code |
|
||||
|---|---|---|---|---|
|
||||
| Margin | 256 | 237 | 114 | 48% |
|
||||
| Margin Calendar | 354 | 279 | 123 | 44% |
|
||||
| Margin Docs | 365 | 291 | 120 | 41% |
|
||||
| Margin Mail | 475 | 386 | 124 | 32% |
|
||||
|
||||
That is 481 lines of one file written four times, 84 to 96 per app excluding brace-only lines, and two
|
||||
copies say so: Calendar `lib.rs:248-249` and Mail `lib.rs:251-252` both carry the comment "Ported from
|
||||
margin's lib.rs". The shell deletes roughly 400 of the 481, plus the four copies of `app_data_dir`
|
||||
(Calendar's and Docs' `library.rs` are byte-identical nine-line files, Margin's copy is
|
||||
`library.rs:49-53`, Mail's `library.rs:8-12`) and the second copies of `packaged_by` and
|
||||
`show_main_window`; `margin-sqlite` takes another 150.
|
||||
|
||||
Six hundred lines out against 600 written once is not the argument. The argument is that the close
|
||||
button has three answers, the schema ladder has two and one does not compose, the transaction boundary
|
||||
is missing where it is most needed, and three apps cannot say why anything failed once the process has
|
||||
exited.
|
||||
|
||||
## 2. The crates to build
|
||||
|
||||
### margin-shell
|
||||
|
||||
**What it owns.** The desktop plugin prologue, the menu scaffold and its event forwarding, the window
|
||||
close policy and the Dock reopen, `app_data_dir`, `packaged_by`, and the update channel logic.
|
||||
|
||||
**The duplication.** The prologue is verbatim in all four (Margin `lib.rs:149-162`, Calendar
|
||||
`250-263`, Docs `250-266`, Mail `253-268`). The menu scaffold, meaning `Menu::default(handle)`, the
|
||||
`submenus` collect, the `find_submenu` closure, the `match find_submenu("File")` with its
|
||||
`prepend_items` and `SubmenuBuilder` arms, the Edit and Help appends and the platform blocks, is Margin
|
||||
`lib.rs:46-60` and `62-93`, Calendar `50-64` and `66-95`, Docs `100-114` and `115-146`, Mail `84-98`
|
||||
and `100-135`; a pairwise diff of the whole `build_menu` puts Calendar against Mail at 49 differing
|
||||
lines out of 118 and 123. The forwarding line is character-identical in all four:
|
||||
`app.emit("menu-action", event.id().0.as_str()).ok();` (Margin `192`, Calendar `308`, Docs `318`, Mail
|
||||
`341`). `show_main_window` is identical between Calendar `227-234` and Mail `184-191`, `packaged_by`
|
||||
between Calendar `239-244` and Mail `196-201` but for the env var name. And Margin's `updates.rs`
|
||||
hardcodes no product name, bundle id or endpoint, reading `handle.config().plugins.0`,
|
||||
`config().identifier` and `package_info().version`, so it drops into the other three unchanged.
|
||||
|
||||
```rust
|
||||
pub fn app_data_dir<R: Runtime>(app: &AppHandle<R>) -> Result<PathBuf, String>;
|
||||
|
||||
/// Process and updater plugins on desktop only, the updater only when the merged config carries an
|
||||
/// `updater` key. Verbatim from all four today.
|
||||
pub fn desktop_plugins<R: Runtime>(builder: Builder<R>, context: &Context<R>) -> Builder<R>;
|
||||
pub enum Row<'a> { Item { id: &'a str, label: &'a str, accel: Option<&'a str> }, Separator }
|
||||
pub enum AppSubmenu { TauriDefault, Trimmed }
|
||||
pub enum Handled { Yes, No }
|
||||
pub enum OnClose { HideWindow, DestroyAndKeepRunning, Quit }
|
||||
|
||||
pub struct MenuSpec<'a> {
|
||||
pub file: &'a [Row<'a>], // prepended, or built fresh when there is no File submenu
|
||||
pub edit_append: &'a [Row<'a>],
|
||||
pub view_prepend: &'a [Row<'a>],
|
||||
pub window_append: &'a [Row<'a>],
|
||||
pub help_append: &'a [Row<'a>],
|
||||
pub check_updates: bool, // suppressed when updates::channel() is "none"
|
||||
pub app_submenu: AppSubmenu,
|
||||
}
|
||||
|
||||
pub fn standard_menu<R: Runtime>(handle: &AppHandle<R>, spec: &MenuSpec<'_>) -> tauri::Result<Menu<R>>;
|
||||
|
||||
/// Builds the menu and installs the forwarder in one call. Every id in the spec is emitted as
|
||||
/// `menu-action` unless the hook claims it, which is what Margin needs for `show-window`
|
||||
/// (`lib.rs:194-197`). The hook defaults to a function returning `Handled::No`.
|
||||
pub fn with_menu<R: Runtime>(builder: Builder<R>, spec: &'static MenuSpec<'static>,
|
||||
hook: fn(&AppHandle<R>, &str) -> Handled) -> Builder<R>;
|
||||
|
||||
pub fn on_close<R: Runtime>(builder: Builder<R>, policy: OnClose) -> Builder<R>;
|
||||
pub fn run<R: Runtime>(app: App<R>, policy: OnClose); // wraps app.run with the matching Reopen arm
|
||||
pub fn show_main_window<R: Runtime>(app: &AppHandle<R>);
|
||||
pub fn packaged_by_env(identifier: &str) -> String; // studio.margin.mail -> MARGIN_MAIL_PACKAGED_BY
|
||||
#[tauri::command] pub fn packaged_by(app: AppHandle) -> Option<String>;
|
||||
|
||||
pub mod updates {
|
||||
pub fn channel<R: Runtime>(handle: &AppHandle<R>) -> &'static str; // direct | appstore | none
|
||||
#[tauri::command] pub fn update_channel(app: AppHandle) -> &'static str;
|
||||
#[tauri::command] pub async fn appstore_latest(app: AppHandle) -> Result<Option<AppStoreRelease>, String>;
|
||||
#[tauri::command] pub fn open_appstore(track_id: u64) -> Result<(), String>;
|
||||
}
|
||||
```
|
||||
|
||||
Deriving the id list from the spec rather than repeating it in a `matches!` guard removes a class of
|
||||
drift: an id can be built into the menu, left out of the guard, and silently do nothing. All four id
|
||||
sets agree today, checked; Margin's one extra, `show-window`, is handled in Rust, which is why the
|
||||
hook exists. `updates::appstore_latest` takes its client from `margin-http` rather than Margin's own
|
||||
`LazyLock<reqwest::Client>` (`updates.rs:6`), since Margin's graph already resolves two reqwest majors.
|
||||
|
||||
**Two open decisions the crate forces.** `AppSubmenu::Trimmed` is Docs' rebuilt macOS app submenu
|
||||
(`lib.rs:164-176` explains why: Services advertises a list the app cannot see or order, and the Hide
|
||||
rows are a window state nobody reaches for through a menu). It is the better implementation and
|
||||
[risks.md](risks.md) says extraction takes the better version, but adopting it costs Cmd+H in the
|
||||
other three, so confirm before flipping them. `OnClose` has three variants because the apps have three
|
||||
behaviours; the crate does not choose, it makes each app name one.
|
||||
|
||||
**What an app still supplies.** The menu contents, the `invoke_handler` list, every `manage` call, the
|
||||
body of `setup`, deep link registration (Calendar and Mail), and `main.rs`. One caveat: the name the
|
||||
frontend invokes is the bare function name even for a command defined in a dependency, so the shared
|
||||
crates own five global names, `update_channel`, `appstore_latest`, `open_appstore`, `packaged_by` and
|
||||
`log_note`.
|
||||
|
||||
### margin-log
|
||||
|
||||
**What it owns.** A single log file in the app data directory, capped, with one line per failure.
|
||||
|
||||
**The duplication.** There is none, and that is the point. Only Mail logs: `src-tauri/src/log.rs`, 137
|
||||
lines, 94 not tests. Docs has six `eprintln!`s (`lib.rs:270`, `:278`, `index.rs:437`, `:596`,
|
||||
`watch.rs:210`, `writingtools.rs:136`), all going nowhere under a Finder launch, and Margin and
|
||||
Calendar have neither: when a Drive backup or a calendar sync fails the string reaches the frontend
|
||||
and the process forgets it. Cheapest crate to write, largest change, because the standing rule for
|
||||
this suite is that an error report starts by reading the log.
|
||||
|
||||
```rust
|
||||
pub const CAP_BYTES: u64 = 256 * 1024;
|
||||
|
||||
/// Told its directory rather than reaching for an `AppHandle`. Mail calls this from `Db::open`
|
||||
/// (`db.rs:46`) so the engine still works under `cargo test`; keep that property.
|
||||
pub fn init(dir: &Path, file_name: &str);
|
||||
pub fn path() -> Option<&'static Path>;
|
||||
|
||||
/// Always to stderr; to the file as well once `init` has run. `who` is an account id or an area.
|
||||
pub fn note(who: &str, line: &str);
|
||||
pub fn note_result(who: &str, line: &str) -> std::io::Result<()>; // same, without dropping the error
|
||||
|
||||
#[tauri::command] pub fn log_note(who: String, line: String);
|
||||
```
|
||||
|
||||
Lift Mail's implementation, `trim` at a 2,000 character line cap and the keep-the-newest-half rewrite
|
||||
in `append` included, and fix two things on the way: `note` discards the result of `append`
|
||||
(`log.rs:52`), so a failed write is invisible, and it does blocking file I/O under a
|
||||
`std::sync::Mutex` from async call sites (`sync/engine.rs:327`, `:363`, `:448`,
|
||||
`sync/hydrate.rs:276`, `google/gmail.rs:106`), which is bounded and infrequent but should not be
|
||||
copied into three more apps unexamined.
|
||||
|
||||
**What an app supplies.** The file name, and the frontend half, 15 lines per app and the half that
|
||||
catches most real failures: the `.catch` in the IPC wrapper that logs every rejected `invoke` (Mail
|
||||
`src/ipc.ts:752-758`) and the `window.onerror` and `unhandledrejection` handlers (Mail
|
||||
`src/main.tsx:20-26`), which belong in `@margin/ipc`. The crate depends on `chrono`, adding it to
|
||||
Margin's and Docs' graphs, a small price for four logs stamped alike.
|
||||
|
||||
### margin-sqlite
|
||||
|
||||
**What it owns.** Opening a connection with the right pragmas, transaction and savepoint guards, the
|
||||
migration runner, the meta table accessors, and an error type that converts.
|
||||
|
||||
**The duplication.** Three apps have SQLite, about 8,500 lines between them, of which 80 to 120 are
|
||||
genuinely the same code. Do not build this for the volume. `version()` is byte-identical between
|
||||
Calendar `store/schema.rs:130-136` and Mail `mirror/schema.rs:127-135`, and identical again modulo the
|
||||
`state.` prefix at Mail `state/schema.rs:56-64`. The `meta` upsert, one `INSERT ... ON CONFLICT DO
|
||||
UPDATE`, exists four times: Calendar `store/write.rs:214-228`, Mail `mirror/write.rs:57-73`, inside
|
||||
both migrate functions, and as Docs' `remember` (`index.rs:891-899`). `now_ms` is identical between
|
||||
Docs `index.rs:905-910` and Mail `mirror/write.rs:42-46`, with Calendar's chrono equivalent at
|
||||
`store/write.rs:87-89`. The placeholder helper is one line under two names, `placeholders`
|
||||
(`index.rs:901-903`) and `holes` (`mirror/read.rs:898-900`). And `.map_err(|e| e.to_string())` appears
|
||||
309 times in the database code alone (Calendar 59, Docs 36, Mail 214), which is not a function waiting
|
||||
to be extracted, it is a `From` impl waiting to be written.
|
||||
|
||||
```rust
|
||||
pub struct Error(String);
|
||||
impl From<rusqlite::Error> for Error;
|
||||
impl From<Error> for String; // so a command can keep returning Result<T, String>
|
||||
|
||||
/// Defaults to WAL, synchronous NORMAL, foreign keys on, no journal size limit (Docs sets one at
|
||||
/// `index.rs:208-221`) and a 5s busy timeout, which only Mail sets today (`db.rs:129-151`).
|
||||
pub struct Pragmas { pub wal: bool, pub synchronous: Synchronous, pub foreign_keys: bool,
|
||||
pub journal_size_limit: Option<i64>, pub busy_timeout: Duration }
|
||||
impl Default for Pragmas;
|
||||
|
||||
pub fn open(path: &Path, pragmas: &Pragmas) -> Result<Connection, Error>;
|
||||
|
||||
/// Takes `&Connection`, not `&mut`. Calendar's `store/write.rs:62-85` already has this shape and it
|
||||
/// is exactly what Mail needs from inside `Db::with`, which hands out a shared reference. Drop
|
||||
/// rolls back.
|
||||
pub struct Tx<'a>;
|
||||
impl<'a> Tx<'a> {
|
||||
pub fn begin(conn: &'a Connection) -> Result<Tx<'a>, Error>; // BEGIN IMMEDIATE
|
||||
pub fn commit(self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub struct Savepoint<'a>; // begin(conn, name) / release, Drop rolls back to it
|
||||
|
||||
pub enum VersionStore {
|
||||
UserPragma, // Docs
|
||||
MetaRow { table: &'static str, key: &'static str }, // Calendar, Mail, and Mail's `state.meta`
|
||||
}
|
||||
|
||||
/// `steps[i]` runs when the stored version is below `i + 1`. Sequential, so V3 composes. Refuses a
|
||||
/// database newer than `steps.len()`, naming `noun` in the message the way all three do today.
|
||||
pub fn migrate(conn: &Connection, steps: &[&str], store: VersionStore, noun: &str) -> Result<(), Error>;
|
||||
|
||||
pub fn meta_get(conn: &Connection, table: &str, key: &str) -> Result<Option<String>, Error>;
|
||||
pub fn meta_set(conn: &Connection, table: &str, key: &str, value: &str) -> Result<(), Error>;
|
||||
pub fn holes(n: usize) -> String; // "?,?,?"
|
||||
pub fn now_ms() -> i64; // SystemTime, not chrono: adds nothing to Margin's or Docs' graph
|
||||
```
|
||||
|
||||
**What an app still supplies.** Every schema, every read, every write, and the connection ownership
|
||||
model, three things each right for its app: `Mutex<Connection>` (Calendar `store/mod.rs:17`),
|
||||
`Mutex<Option<Connection>>` behind a writer thread (Docs `index.rs:143-151`), and
|
||||
`Mutex<HashMap<String, Connection>>` with one pair of ATTACHed files per account (Mail `db.rs:31-38`),
|
||||
whose formatted ATTACH stays in Mail because ATTACH takes no bound parameter.
|
||||
|
||||
## 3. The crates not to build
|
||||
|
||||
**No settings crate.** Only Mail has settings in Rust: `settings.rs`, 479 lines, a recursive JSON
|
||||
merge for patches (`merge_into`, `settings.rs:234`), an atomic write, and a deliberate refusal to
|
||||
reset on a malformed file (`settings.rs:210`, tested at `:430`). The other three keep preferences in
|
||||
`localStorage`, 16 keys in Margin, five in Calendar, seventeen in Docs; Calendar's whole Settings
|
||||
screen edits one preference, week start day. Lifting this means inventing a settings backend for three
|
||||
apps that do not have one, a feature rather than a refactor, and the 25-field struct cannot move
|
||||
regardless. The reusable core, if wanted, is 60 lines.
|
||||
|
||||
**No error crate.** Every one of the 165 `#[tauri::command]`s across the four apps returns either a
|
||||
bare value or `Result<T, String>`, with zero exceptions: Margin 24, Calendar 13, Docs 38, Mail 90.
|
||||
Counts of `-> Result<T, String>` anywhere are 52, 94, 91 and 485, and neither `thiserror` nor `anyhow`
|
||||
is a dependency of any of them. The custom enums are internal and never reach the frontend, and the
|
||||
two that share a name are not the same type: Calendar's `ApiError` (`google/api.rs:132`) needs
|
||||
`SyncTokenExpired` and `PreconditionFailed`, Mail's (`google/api.rs:184`) needs `Unauthorized`,
|
||||
`InsufficientScope` and `Dropped`, and even their `From<reqwest::Error>` differs deliberately, with a
|
||||
comment in Mail on why Calendar's classification would be wrong for a mail client waking from sleep.
|
||||
The one useful piece, the `From<rusqlite::Error>` that kills 309 `.map_err`, lives in `margin-sqlite`.
|
||||
|
||||
**No filesystem crate.** Four `atomic_write`s, three genuinely different algorithms. Docs'
|
||||
(`fs.rs:311-347`, helpers at `:222-288`) carries a per-path `Arc<Mutex<()>>` lock map so a debounced
|
||||
autosave cannot race Cmd+S, copies the original into the temp so macOS ACLs, Finder tags and the exec
|
||||
bit survive, uses a hidden collision-retried temp name, and makes four `watch::note_self_write` calls;
|
||||
Margin's (`project.rs:13-30`) adds `.bak` rotation, Mail's (`library.rs:19-33`) adds `create_dir_all`.
|
||||
Sharing one either drops Docs' watcher integration and lock map or drags the file watcher into the
|
||||
shared crate. Path validation is the same story: Docs has the only real gate (`checked` at
|
||||
`fs.rs:212-214`), Mail never lets the frontend name a write target, and Margin has none, which is a
|
||||
defect in Margin rather than an argument for a crate. Trash is one app; file watching is one app.
|
||||
|
||||
**No async crate.** Margin has no tokio dependency at all, and Docs declares one at `Cargo.toml:34`
|
||||
and never uses it: grep for `tokio::` in its `src-tauri/src` returns nothing.
|
||||
`tauri::async_runtime::spawn` is the house style in the three apps that spawn. The two poll loops
|
||||
(Calendar `sync.rs:204-215`, Mail `sync/mod.rs:374-386`) share a skeleton, `FIRST_PASS_SECS = 2` and a
|
||||
character-identical `focused()`, then diverge on what matters: Calendar awaits a `tokio::sync::Notify`
|
||||
with a timeout so `kick` can pull the next tick forward, Mail does a bare `sleep` and has `kick` spawn
|
||||
a separate `sync_now` behind an `AtomicBool` guard. The one abstraction arrived at twice, the `Sink`
|
||||
trait (Calendar `sync.rs:68-78`, Mail `sync/mod.rs:230-266`), is 12 lines and belongs with the sync
|
||||
engine, which is not shared. The lock rule all three follow, `std::sync::Mutex` for SQLite behind a
|
||||
`with(|conn| ...)` closure and `tokio::sync::Mutex` for anything held across an `.await`, goes in the
|
||||
guidelines and is not code.
|
||||
|
||||
**No DTO macro.** One convention, held rigidly across 65 structs: `#[derive(Debug, Clone, Serialize,
|
||||
Deserialize)]` with `#[serde(rename_all = "camelCase")]`, `#[serde(default)]` on patch fields,
|
||||
`Option<T>` rather than a sentinel, `i64` epoch milliseconds for time, and a string field with the
|
||||
legal values in a doc comment instead of an enum; `#[serde(rename = ...)]` appears exactly once in the
|
||||
suite (Calendar `dto.rs:36`). A derive macro would save one line per struct and put a proc-macro crate
|
||||
in four build graphs. What costs something is that each of those 65 structs has a hand-written
|
||||
TypeScript interface in `src/ipc.ts` with nothing checking they agree, and the first 41 lines of
|
||||
Calendar's and Docs' `ipc.ts` are byte identical: a codegen question, `ts-rs` or `tauri-specta`, for
|
||||
the frontend plan.
|
||||
|
||||
## 4. The defects found on the way
|
||||
|
||||
Ranked by what a user loses. "Ride along" means the extraction fixes it; "before" means fix it in
|
||||
place first, because the extraction would otherwise carry a broken line forward.
|
||||
|
||||
**1. Mail applies flags and queues the push without a transaction.** `apply_and_queue`
|
||||
(`mirror/mod.rs:167-205`) runs `apply_flags` and `outbox::queue_flags` for N messages, per account,
|
||||
unwrapped. Grepping the crate for `unchecked_transaction`, `BEGIN IMMEDIATE`, `.transaction()` and
|
||||
`SAVEPOINT` returns three hits: two `execute_batch("BEGIN;")` in the migrations (`mirror/schema.rs:37`,
|
||||
`state/schema.rs:33`) and one savepoint at `state/journal.rs:278`. A failure mid-loop leaves a flag
|
||||
changed locally with no outbox row, so it never reaches the server, or the reverse. Every Mail user,
|
||||
silently. **Before**, with Calendar's `Tx` shape, which the shared crate then replaces.
|
||||
|
||||
**2. Margin takes an arbitrary absolute path from the webview.** `project.rs:33`, `:38` and `:43`
|
||||
expose `read_file`, `write_file` and `write_bytes` as commands taking a `String` path with no
|
||||
validation at all; the only checked path in the crate is the book id whitelist at `library.rs:61-66`.
|
||||
Script execution in the webview becomes arbitrary read and write with the app's privileges, and Margin
|
||||
renders prose from files it did not write. Docs has the answer to copy, `checked` at `fs.rs:212-214`.
|
||||
**Before**, and not blocked on any of this work.
|
||||
|
||||
**3. Docs and Mail ship a placeholder updater pubkey.** `src-tauri/tauri.release.conf.json:8` reads
|
||||
`REPLACE_WITH_TAURI_SIGNER_PUBKEY` in Docs and `REPLACE_WITH_THE_MINISIGN_PUBLIC_KEY` in Mail, and
|
||||
neither workflow substitutes it: both only set `TAURI_SIGNING_PRIVATE_KEY` (Docs `release.yml:203-204`,
|
||||
Mail `:207-208`). Margin and Calendar have real keys. Every direct-download install of two apps is
|
||||
stranded on the version it was downloaded at, with no verifiable update path. **Before**; a release
|
||||
concern rather than a crate one, but `margin-shell` owning the channel logic is when it stops hiding.
|
||||
|
||||
**4. Mail's `Settings` has 25 fields and one `#[serde(default)]`.** `dto.rs:768-813`, the single
|
||||
default at `:805` on `notifications`. Every other field is required, so the next field added without
|
||||
one fails to parse every existing install's `settings.json` and `settings_get` (`settings.rs:144`)
|
||||
errors out for good, the deliberate no-reset-on-malformed policy (`settings.rs:210`) keeping it that
|
||||
way. There is a regression test for the one field that has a default (`settings.rs:389`); the pattern
|
||||
was never generalised. Every Mail user, on the first upgrade after the mistake. **Before**:
|
||||
`#[serde(default)]` on all 25 plus a `Default` impl backed by `defaults()` (`settings.rs:32`).
|
||||
|
||||
**5. Margin Mail cannot compile for mobile.** `#[cfg_attr(mobile, tauri::mobile_entry_point)]` sits at
|
||||
`lib.rs:203`, directly above `attach_account`, not above `pub fn run()` at `lib.rs:250`. Separately,
|
||||
`setup` calls `listen_for_redirects` (`lib.rs:307`), `stop_uikit_shrinking_the_viewport` (`:310`) and
|
||||
`watch_for_the_consent_tab_closing` (`:314`) under `cfg(mobile)`, `cfg(target_os = "ios")` and
|
||||
`cfg(target_os = "android")`, and none of the three is defined anywhere in the crate: grep returns the
|
||||
call sites and nothing else. All three exist in Calendar (`lib.rs:150-184`, `186-212`, `214-226`) and
|
||||
were meant to be ported with the rest, the iOS and Android dependency blocks being already in Mail's
|
||||
`Cargo.toml`. Nobody is affected today because no mobile build runs; everybody is, the first day one
|
||||
does. **Fix the attribute before**; the two webview helpers then **ride along** into `margin-shell`
|
||||
and `listen_for_redirects` into `margin-google`.
|
||||
|
||||
**6. Four apps, three window close behaviours.** Calendar (`lib.rs:313-321`) and Mail (`:346-354`)
|
||||
prevent the close and hide the window, then restore on `RunEvent::Reopen` (Calendar `:342-348`, Mail
|
||||
`:463-469`). Margin lets the window be destroyed but calls `api.prevent_exit()` (`lib.rs:234`) and
|
||||
rebuilds from config on Reopen (`open_main_window`, `:241-256`). Docs calls `.run(context)` at
|
||||
`lib.rs:363` with no `RunEvent` closure and no `CloseRequested` handler anywhere in the crate, so
|
||||
closing the window quits the app and unsaved state goes with it. **Rides along**: `OnClose` makes each
|
||||
app name its policy, and Docs' is the one to reopen with the user.
|
||||
|
||||
**7. Calendar's migration ladder will not compose.** `store/schema.rs:115-120` is
|
||||
`if found < 1 { V1 } else if found < 2 { V2 }`. An `else if`, so a database at version 0 when V3 lands
|
||||
runs V1, stops, and gets stamped as current. Mail's sequential `if`s (`mirror/schema.rs:39-44`) are
|
||||
correct. Latent and total: every Calendar user with an old database, the day a third migration ships.
|
||||
**Rides along**: `margin_sqlite::migrate` runs every step below the target.
|
||||
|
||||
**8. Four smaller ones.** No `atomic_write` in the suite fsyncs the parent directory, so a crash can
|
||||
still lose the rename; Margin's also `remove_file`s the target before renaming when `backup` is false
|
||||
(`project.rs:26`), and Mail's `with_extension` (`library.rs:23-26`) doubles the dot for a path with no
|
||||
extension. `note` discards the result of `append` (`log.rs:52`), so a failed log write is invisible:
|
||||
fix that one before publishing `margin-log`. Margin's window permissions sit in
|
||||
`capabilities/default.json:8-9` rather than `desktop.json`, so they apply on phones, and Docs alone
|
||||
carries `core:window:allow-toggle-maximize`. And `prepare_cached` appears zero times in the three apps
|
||||
with a database, `still_bodiless` (`mirror/read.rs:949-969`) preparing inside a loop.
|
||||
|
||||
## 5. Version drift
|
||||
|
||||
Agreed everywhere and not worth a row: `tauri` and `tauri-build` at 2, `serde` and `serde_json` at 1,
|
||||
`base64` 0.22, `tauri-plugin-opener` 2, `objc2` 0.6, `objc2-foundation` 0.3, and where shared, `sha2`
|
||||
0.10, `rand` 0.8, `url` 2, `chrono` 0.4, `tempfile` 3, `harper-core` =2.5.0, `typst` and `typst-pdf`
|
||||
0.14.2, `typst-as-lib` 0.15.5, `tauri-plugin-dialog` and `tauri-plugin-deep-link` at 2. Where they
|
||||
disagree, blank meaning the app does not have it:
|
||||
|
||||
| Crate | Margin | Calendar | Docs | Mail | Land on |
|
||||
|---|---|---|---|---|---|
|
||||
| rusqlite | | **0.37** | 0.40 | 0.40 | 0.40 |
|
||||
| reqwest | **0.12** | **0.12** | | 0.13 | 0.13 |
|
||||
| chacha20poly1305 | | **0.10** | | 0.11 | 0.11 |
|
||||
| fontdb | **0.23** | | **0.23** | 0.24 | 0.24 |
|
||||
| tokio | none | 1 (sync, time) | 1 (**unused**) | 1 (sync, time, net, io-util, rt) | drop from Docs |
|
||||
| tauri-plugin-process | 2 (**ungated**) | 2 gated | 2 gated | 2 gated | gate in Margin |
|
||||
| tauri-plugin-updater | 2 (**ungated**) | 2 gated | 2 gated | 2 gated | gate in Margin |
|
||||
|
||||
Resolved in the lockfiles: `tauri` 2.11.3 in Margin against 2.11.5 elsewhere, `serde` 1.0.228 against
|
||||
1.0.229, `tokio` 1.52.3 against 1.53.1, `libsqlite3-sys` 0.35.0 in Calendar against 0.38.2 in Docs and
|
||||
Mail, `reqwest` 0.13.1 in Mail against 0.13.4 elsewhere; `wry` 0.55.1 and `objc2` 0.6.4 are uniform.
|
||||
Margin's, Calendar's and Mail's graphs each carry two reqwest majors already, 0.12.28 and 0.13.x,
|
||||
because `tauri-plugin-updater` pulls 0.13 whatever the app declares, so moving the direct dependency
|
||||
to 0.13 collapses that to one copy in three apps.
|
||||
|
||||
These matter beyond tidiness. rusqlite 0.37 against 0.40 is a hard blocker for `margin-sqlite`, since
|
||||
one crate cannot compile against two rusqlite majors in one graph, so Calendar moves first. Calendar
|
||||
and Mail share a sealed-token format, so chacha20poly1305 0.10 against 0.11 is a split inside a pair
|
||||
meant to be one implementation, and Margin and Docs share a Typst pipeline, so fontdb matters the day
|
||||
`margin-typeset` lands. The shared crates themselves land on `rusqlite` 0.40 with `bundled` (the
|
||||
feature that brings FTS5, already commented in Docs' and Mail's manifests), `tauri` 2 with default
|
||||
features off, `chrono` 0.4 in `margin-log` only, and no reqwest in `margin-shell` at all.
|
||||
|
||||
## 6. Four repos, no workspace, one with no remote
|
||||
|
||||
Margin has a remote and 149 commits. Calendar has a remote, 27 commits and a clean tree. Docs has a
|
||||
remote, 8 commits and 129 uncommitted files. **Margin Mail has no remote at all**, one scaffold commit
|
||||
and 123 uncommitted files. There is no cargo workspace spanning them and no path dependency between
|
||||
them.
|
||||
|
||||
A cargo path dependency walking out of one checkout into a sibling would fail on a fresh clone and in
|
||||
CI in exactly the way the npm one already does. That is not a prediction: Docs' `package.json:33` and
|
||||
Mail's `package.json:27` both read `"margin-shared": "file:../../python/margin/shared"`, which resolves
|
||||
on this machine because of the order things were created in and nowhere else. Writing
|
||||
`margin-log = { path = "../../python/margin-shared/crates/margin-log" }` reproduces it with a
|
||||
different tool. So the mechanism is a cargo git dependency on the fifth repo, pinned to a tag:
|
||||
|
||||
margin-shell = { git = "https://github.com/priyanshujain/margin-shared", tag = "v0.3.0" }
|
||||
|
||||
Cargo needs no registry for this, which is the one place Rust has it easier than npm. Three
|
||||
consequences. Each app's `Cargo.lock` records the resolved git rev and stays committed, so moving a
|
||||
tag changes nothing until someone runs `cargo update -p margin-shell`: treat tags as immutable. Local
|
||||
iteration goes through a `[patch]` section or a `paths` entry in `.cargo/config.toml`, and per
|
||||
[risks.md](risks.md) that is a switch a developer turns on, never a value the repo ships. And the
|
||||
shared repo's CI has to build all four apps against a candidate before a tag is cut, which needs all
|
||||
four checkoutable from CI, which needs Mail to have a remote. Nothing starts until Docs' 129 files and
|
||||
Mail's 123 files are committed and pushed; that is a precondition, not a precaution.
|
||||
|
||||
## 7. Per app, exactly what changes
|
||||
|
||||
**Step 0, the shared repo.** Create the three crates under `crates/` in the MIT-licensed
|
||||
`margin-shared` repository. `margin-log` first: 100 lines, no dependents among the other two, and the
|
||||
only one that gives three apps a capability they lack. Then `margin-sqlite`, then `margin-shell`, which
|
||||
needs `margin-http` for the App Store probe and so lands after [accounts.md](accounts.md)'s first
|
||||
crate. Tag `v0.3.0`.
|
||||
|
||||
**Margin Mail** first: it is the source of `margin-log`, the worst affected by the missing
|
||||
transactions, and the one needing the mobile fix.
|
||||
|
||||
1. Give it a remote and commit the 123 files.
|
||||
2. Fix `apply_and_queue` with a local `Tx`, put `#[serde(default)]` on all 25 `Settings` fields, move
|
||||
the attribute at `lib.rs:203` onto `pub fn run()` at `:250` (defects 1, 4, 5).
|
||||
3. Take `margin-log`; delete `log.rs` but keep the `log::init(&data)` call in `Db::open` (`db.rs:46`).
|
||||
4. Take `margin-sqlite`: replace both `version()`s, both `meta` upserts, `holes`, `now_ms` and the two
|
||||
migrate skeletons; keep the ATTACH at `db.rs:129-151`; convert the 214 `.map_err` to `?`.
|
||||
5. Take `margin-shell`: delete `library.rs:8-12`, `show_main_window`, `packaged_by`, the prologue and
|
||||
`build_menu`, the last becoming a `MenuSpec` const. `OnClose::HideWindow`, which is today's
|
||||
behaviour. Register `margin_shell::updates::*` and put a real updater pubkey in the release config.
|
||||
6. Port Calendar's three mobile helpers, or drop the three call sites until mobile is real.
|
||||
|
||||
**Margin Calendar** second: the only clean tree, and the only app that has to move a rusqlite major.
|
||||
|
||||
1. Bump `rusqlite` 0.37 to 0.40 and `chacha20poly1305` 0.10 to 0.11; run the store tests.
|
||||
2. Take `margin-sqlite`: `Tx` and `Savepoint` leave `store/write.rs:62-85`, `version()`, `meta_get`,
|
||||
`meta_set` and `now_ms` are deleted, and `migrate` (`store/schema.rs:105-128`) takes a `&[&str]` so
|
||||
the `else if` at `:117` stops being a bug.
|
||||
3. Take `margin-log`, initialise it where the store opens, and route through `note` the errors that
|
||||
currently only reach the frontend.
|
||||
4. Take `margin-shell`: delete `library.rs`, `show_main_window`, `packaged_by`, the prologue and
|
||||
`build_menu`. `OnClose::HideWindow`. Register the updates commands, which Calendar lacks.
|
||||
|
||||
**Margin Docs** third.
|
||||
|
||||
1. Commit the 129 files. Drop the unused `tokio` line at `Cargo.toml:34`; bump `fontdb` to 0.24.
|
||||
2. Put a real pubkey in `tauri.release.conf.json:8` and make the workflow assert it is not a
|
||||
placeholder.
|
||||
3. Take `margin-log` and convert the six `eprintln!`s into `note` calls. Largest behavioural gain of
|
||||
the whole plan for Docs.
|
||||
4. Take `margin-sqlite` with `VersionStore::UserPragma`, keeping the writer thread and the
|
||||
`journal_size_limit` pragma, which becomes a `Pragmas` field.
|
||||
5. Take `margin-shell`. Docs' rebuilt macOS app submenu is what the crate adopts, so this is a
|
||||
deletion here and a change for the other three. Choose an `OnClose`: today it is `Quit` by omission.
|
||||
|
||||
**Margin** last: the most local history, the least to gain.
|
||||
|
||||
1. Fix `project.rs:33`, `:38` and `:43` with Docs' `checked`, and the remove-then-rename window at
|
||||
`project.rs:26`.
|
||||
2. Move `tauri-plugin-process` and `tauri-plugin-updater` from `[dependencies]` (`Cargo.toml:25-26`)
|
||||
into the `cfg(not(android, ios))` target block, and the two window permissions from
|
||||
`capabilities/default.json:8-9` into `desktop.json`. Bump `reqwest` to 0.13.
|
||||
3. Take `margin-log`; Margin currently logs nothing at all.
|
||||
4. Take `margin-shell`: `updates.rs` moves out wholesale and comes back as a dependency,
|
||||
`app_data_dir` leaves `library.rs:49-53`, `build_menu` becomes a `MenuSpec` with the `show-window`
|
||||
hook, and `OnClose::DestroyAndKeepRunning` preserves today's behaviour. Add `packaged_by` and tell
|
||||
the Nix wrapper the variable is `MARGIN_APP_PACKAGED_BY`.
|
||||
5. No `margin-sqlite`: Margin has no database.
|
||||
Reference in new issue
Block a user