# mizan-tauri Tauri backend adapter for the Mizan protocol. One plugin call on the Rust side. `#[mizan::client]` on async functions. Typed React client generated. Invalidation automatic — same protocol surface as mizan-fastapi / mizan-django / mizan-rust-axum, routed through Tauri's IPC instead of HTTP. ## Scope mizan-tauri targets the **AFI-common subset** — RPC dispatch, context bundling, server-driven invalidation/merge. The transport channel is Tauri's `invoke()`; the dispatch table is the linkme-backed `FUNCTIONS` slice from `mizan-core`. No HTTP server is involved — the Tauri runtime handles message framing, the plugin handles dispatch. Forms / SSR / Channels are out of scope (those are Django-side primitives). Tauri apps using mizan-tauri get RPC + context bundling + invalidation, nothing more. ## Install ```toml # src-tauri/Cargo.toml [dependencies] tauri = "2" mizan-core = { path = "../../mizan/cores/mizan-rust" } mizan-tauri = { path = "../../mizan/backends/mizan-tauri" } serde = { version = "1", features = ["derive"] } ``` ```jsonc // package.json { "dependencies": { "@mizan/base": "file:../mizan/frontends/mizan-base", "@mizan/tauri-transport": "file:../mizan/frontends/mizan-tauri-transport", "@tauri-apps/api": "^2" } } ``` ## Setup — Rust Install the plugin on the Tauri builder. The plugin registers a single command (`plugin:mizan|mizan_invoke`) that routes call/fetch envelopes through the function registry. No per-function `#[tauri::command]` is needed; the macro-emitted FunctionSpec IS the dispatch table. ```rust // src-tauri/src/lib.rs mod commands; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(mizan_tauri::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` `commands` must be reachable from the binary's link graph — `mod commands;` works (private mod stays linked because `lib.rs` references it through file inclusion). If a separate binary (e.g. the IR-export bin below) also needs to see the registrations, mark it `pub mod commands;` so the integration-test / sibling-binary path can force-link. ## Define server functions ```rust // src-tauri/src/commands.rs use mizan_core::{self as mizan, MizanError, RequestHandle}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, mizan_core::Mizan)] pub struct Greeting { pub message: String, } #[mizan::client] pub async fn greet(_req: &RequestHandle<'_>, name: String) -> Greeting { Greeting { message: format!("hello, {name}") } } // Result is supported when the function can fail; the // dispatch wrapper `?`-unwraps it so server-side errors surface as the // protocol's standard {code, message, details?} envelope. #[mizan::client] pub async fn read_file( _req: &RequestHandle<'_>, path: String, ) -> Result { let body = std::fs::read_to_string(&path) .map_err(|e| MizanError::NotFound(e.to_string()))?; Ok(Greeting { message: body }) } ``` `#[mizan::client]` parameters mirror the other backends — `context = …`, `affects = …`, `merge = …`, `private`. See `mizan-rust-axum`'s README for the full set. ### App-state access The first parameter is `req: &RequestHandle<'_>` — the same handle the HTTP adapter threads through. Inside a Tauri-mounted plugin, the handle wraps `tauri::AppHandle`, so user functions can downcast for access to Tauri's managed-state container or event emission: ```rust #[mizan::client] pub async fn store_value(req: &RequestHandle<'_>, key: String) -> Greeting { let app = req.downcast::() .expect("Tauri AppHandle threaded by mizan-tauri"); // app.state::(), app.emit(...), etc. Greeting { message: format!("stored {key}") } } ``` Stateless functions ignore the handle (`_req: &RequestHandle<'_>`). ## IR export binary mizan-generate needs the consumer crate's IR. Add a small bin that references each `#[mizan::client]` function (so linkme keeps the distributed slice's entries) and prints `mizan_core::build_ir()`: ```rust // src-tauri/src/bin/emit_mizan_ir.rs // // Cargo.toml adds: // [[bin]] // name = "emit-mizan-ir" // path = "src/bin/emit_mizan_ir.rs" // // linkme only collects from translation units that survive // dead-code elimination; this fn names one item per file carrying // #[derive(Mizan)] / #[mizan::client] registrations so the linker // keeps them in the final binary. #[allow(dead_code)] fn _force_link() { use my_app_lib::commands; let _ = commands::greet; let _ = commands::read_file; // ... one per #[mizan::client] function } fn main() { _force_link(); print!("{}", mizan_core::build_ir()); } ``` ## Generate the frontend ```toml # mizan.toml at the project root project_id = "my-tauri-app" output = "src/api" targets = ["react"] [source.rust] manifest_path = "src-tauri/Cargo.toml" bin = "emit-mizan-ir" # Optional — author the Rust types from Pydantic models via decoru. # Omit this block for pure-Rust usage. [source.rust.pydantic] module = "my_app.schema" output = "src-tauri/src/schema.rs" command = ["uv", "run", "python"] # any python with `decoru` importable header = """\ // AUTO-GENERATED by mizan-generate (source.rust.pydantic step). // Source of truth: my_app/schema.py. // DO NOT EDIT BY HAND. Regenerate with: `mizan-generate` use serde::{Deserialize, Serialize}; """ ``` ```bash mizan-generate --config mizan.toml ``` The Pydantic pre-step auto-discovers `BaseModel` subclasses AND `Enum` subclasses declared in the named module; decoru emits the structs, and a small inline emitter renders enums (PascalCase variants from Python member names, `#[serde(rename_all = "snake_case")]`, `#[default]` on the last variant so decoru's `impl Default` keeps compiling). The Rust step then runs `cargo run --bin emit-mizan-ir`, parses the emitted KDL, and dispatches the configured `targets` to their emitters (`stage1` → typed `callXxx`/`fetchXxx`; `react` → `` + per-context providers + `use{Hook}()` hooks). ## Setup — TS ```tsx // src/main.tsx import { configure } from "@mizan/base"; import { tauriTransport } from "@mizan/tauri-transport"; // Route every mizanCall / mizanFetch through Tauri's IPC. Must run // before any generated callXxx() executes — top-level at the module // entry is the safe place. configure({ transport: tauriTransport() }); ``` ```tsx // any component import { callGreet } from "@/api"; const greeting = await callGreet({ name: "world" }); console.log(greeting.message); ``` For framework hooks generated by Stage 2 (`useGreet()` etc., wrapping the imperative `callGreet` with `isPending`/`error` state), wrap your tree with `` at the root — same as the HTTP-transport setup. The generated provider is transport-agnostic; it reads from `config.transport` the kernel is using. ### tsconfig / vite preserve symlinks The `@mizan/*` packages are typically linked via `file:` in package.json. Without `preserveSymlinks`, both TypeScript and Vite/Rollup follow the symlinks to their real location and fail to resolve the linked packages' peer dependencies (`@tauri-apps/api`, `@mizan/base`) from there. ```jsonc // tsconfig.json { "compilerOptions": { "moduleResolution": "bundler", "preserveSymlinks": true, // … } } ``` ```ts // vite.config.ts import { defineConfig } from "vite"; export default defineConfig({ resolve: { preserveSymlinks: true, }, // … }); ``` ## Wire protocol Same envelope as the HTTP adapter, wrapped in a Tauri invoke payload: ```ts // call invoke('plugin:mizan|mizan_invoke', { envelope: { op: 'call', fn: 'greet', args: { name: 'world' } } }) // → { result: { message: "hello, world" }, invalidate: [], merge?: [...] } // fetch (context bundling) invoke('plugin:mizan|mizan_invoke', { envelope: { op: 'fetch', context: 'user', params: { user_id: 42 } } }) // → { user_profile: {...}, user_orders: [...] } (flat bundle) ``` Errors flow through Tauri's `Promise.reject` path; `@mizan/tauri-transport` re-wraps them into the same `MizanError` shape the HTTP transport produces, so consumer code is identical regardless of transport. ## Reference application `claude-manage` is the production reference — Tauri + React + Pydantic schema + Mizan RPC. See `~/dev/claude-manage/mizan.toml` and `~/dev/claude-manage/src-tauri/src/commands.rs` for a full migrated app. ## Architecture mizan-tauri shares `cores/mizan-rust` with `mizan-rust-axum`. Both adapters dispatch through the same compile-time `FUNCTIONS` registry, same `compute_invalidation` / `compute_merges` logic, same KDL IR emitted by `build_ir()`. The only difference is the wire surface — axum takes POST `/call/` and GET `/ctx/:name/`, mizan-tauri takes a single `mizan_invoke` command with an op-tagged envelope.