Typed IPC contracts
Rust is the source of truth for application commands. ipc_builder() in
src-tauri/src/lib.rs supplies both Tauri’s runtime handler and tauri-specta’s
exporter. Add commands to this registry with #[tauri::command] and
#[specta::specta]; use commands from src/lib/generated/ipc.ts in the frontend,
not handwritten invoke<T> calls. Tauri plugin APIs keep their upstream wrappers.
Generation and verification
pnpm generate:ipcpnpm check:ipcpnpm checkcargo test --locked --manifest-path src-tauri/Cargo.toml --libpnpm testGeneration uses the headless export_bindings Cargo example, the committed lockfile,
and the project’s Biome formatter. It does not launch a webview, initialize telemetry,
or read user settings/sessions. Commit src/lib/generated/ipc.ts alongside Rust
contract changes. check:ipc generates into a temporary directory and compares the
formatted bytes without overwriting the tracked file. The Checks workflow runs
this check and TypeScript compile-time checks on macOS. The Tests workflow runs
native tests and the application suite on macOS. Both workflows run only on pull
requests, with the existing repository/actor restrictions. A supported native
Tauri build environment is required even for generation.
The migration uses Tauri 2.11.5+, tauri-specta and Specta =2.0.0-rc.25, and
specta-typescript =0.0.12. The previous Tauri 2.9.x Specta integration uses attributes
removed in Specta rc.25. Keep the prerelease family pinned together and regenerate
and test when upgrading; do not independently update one prerelease dependency.
On macOS with a non-Apple cc on PATH, use Apple’s Clang for Cargo’s linker and
C/C++ build scripts (CC=/usr/bin/clang, CXX=/usr/bin/clang++, and, on Apple Silicon,
CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/clang).
Session boundary
src-tauri/src/sessions/commands.rs dispatches scoped operations directly into the
typed response enum defined in sessions/ipc.rs. Read pages, append/recovery/delete
receipts, recording status, and asset references share their plain native result
types with IPC. There are no duplicate DTOs or field mappings for these values, and
no intermediate JSON serialization/deserialization. The sanitized native Error
is also shared, exported under the existing TypeScript name StorageError.
Separate DTOs remain for scoped registrations and filesystem path conversion,
including discovery diagnostics; non-UTF-8 paths still reject rather than being
lossily converted. Shared output types derive Serialize and Specta’s Type, not
Deserialize. Actual request types retain deserialization and runtime validation.
Filesystem handles, locks, and writer capabilities remain native-only; deriving
Type does not register a command or authorize access.
The IPC request is a generated tagged union, not a JSON-string envelope. Responses
are tagged by the same operation and carry a concrete result. SessionRepository
keeps application decoding, error sanitization, paging, and recording lifecycle
abstractions. Its injectable transport infers results from the operation and checks
the returned discriminant; callers cannot choose an arbitrary response type.
Record inputs (header, entries, started, finished, interrupted) remain
bounded JSON strings. Rust’s existing record framing, depth checks, batch limits,
ancestry checks, path authorization, and media validation still apply. The aggregate
request limit is retained after Tauri deserialization. Record outputs intentionally
remain their original serde_json::Value trees: TypeScript’s existing session
schemas own that extensible vocabulary, and the repository decodes records at
runtime. The Rust JsonValue enum describes only the generated TypeScript schema;
records are not converted into that second representation. Assets and non-record
receipts have explicit generated DTOs. Optional application conveniences (such as
omitting MediaDescription.capture) are normalized before IPC; generated native
nullable fields remain explicit.
Rust integer offsets are exported as JavaScript numbers, matching existing IPC.
Native media validation retains its safe-integer and size limits; generated types
cannot validate integer ranges, finite numbers, filesystem permissions, provider
content, or session invariants. They supplement, never replace, runtime validation.
Command rejections still throw; SessionStorageError continues to sanitize unknown
native errors rather than treating TypeScript types as trustworthy runtime data.
The native integration bridge uses the same typed dispatcher as Tauri and serializes
responses only at the test transport boundary. Tests cover
success/error serialization, malformed requests, record and aggregate limits,
nullable fields, recording recovery, media, and existing persistence behavior.
src/lib/sessions/ipc.type-test.ts uses @ts-expect-error to prevent request/return
contracts from silently weakening. Storage layout and persistence responsibilities
are unchanged.