Move the RPC wire schema into a Tauri-free crate (#553)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Gregory Schier
2026-08-15 11:25:27 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent 93001e3da7
commit 4838353585
29 changed files with 1156 additions and 995 deletions
Generated
+17
View File
@@ -11035,6 +11035,7 @@ dependencies = [
"yaak-models",
"yaak-plugins",
"yaak-rpc",
"yaak-rpc-schema",
"yaak-sse",
"yaak-sync",
"yaak-system-appearance",
@@ -11372,6 +11373,22 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "yaak-rpc-schema"
version = "0.0.0"
dependencies = [
"serde",
"ts-rs",
"yaak-git",
"yaak-grpc",
"yaak-models",
"yaak-plugins",
"yaak-sse",
"yaak-sync",
"yaak-templates",
"yaak-ws",
]
[[package]]
name = "yaak-sse"
version = "0.1.0"
+2
View File
@@ -5,6 +5,7 @@ members = [
# Common/foundation crates
"crates/common/yaak-database",
"crates/common/yaak-rpc",
"crates/common/yaak-rpc-schema",
# Shared crates (no Tauri dependency)
"crates/yaak-core",
"crates/yaak-common",
@@ -63,6 +64,7 @@ ts-rs = "11.1.0"
# Internal crates - common/foundation
yaak-database = { path = "crates/common/yaak-database" }
yaak-rpc = { path = "crates/common/yaak-rpc" }
yaak-rpc-schema = { path = "crates/common/yaak-rpc-schema" }
# Internal crates - shared
yaak-core = { path = "crates/yaak-core" }
+1 -1
View File
@@ -1,6 +1,6 @@
import type { RpcPayload } from "@yaakapp-internal/platform";
import { platform } from "@yaakapp-internal/platform";
import type { RpcSchema } from "@yaakapp-internal/tauri-client";
import type { RpcSchema } from "@yaakapp-internal/rpc-schema";
/**
* Every backend command the app can call: the generated wire schema, one field
+1 -1
View File
@@ -1,5 +1,5 @@
import type { HttpRequest } from "@yaakapp-internal/models";
import type { EphemeralHttpResponse } from "@yaakapp-internal/tauri-client";
import type { EphemeralHttpResponse } from "@yaakapp-internal/rpc-schema";
import { getActiveCookieJar } from "../hooks/useActiveCookieJar";
import { rpc } from "./rpc";
+1
View File
@@ -73,6 +73,7 @@ url = "2"
tokio-util = { version = "0.7", features = ["codec"] }
ts-rs = { workspace = true }
yaak-rpc = { workspace = true }
yaak-rpc-schema = { workspace = true }
uuid = "1.12.1"
yaak-api = { workspace = true }
yaak-common = { workspace = true }
-4
View File
@@ -1,7 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type GitWatchResult = { unlistenEvent: string, };
export type PluginUpdateInfo = { name: string, currentVersion: string, latestVersion: string, };
export type PluginUpdateNotification = { updateCount: number, plugins: Array<PluginUpdateInfo>, };
@@ -12,8 +10,6 @@ export type UpdateResponse = { "type": "ack" } | { "type": "action", action: Upd
export type UpdateResponseAction = "install" | "skip";
export type WatchResult = { unlistenEvent: string, };
export type YaakNotification = { timestamp: string, timeout: number | null, id: string, title: string | null, message: string, color: string | null, action: YaakNotificationAction | null, };
export type YaakNotificationAction = { label: string, url: string, };
+3 -3
View File
@@ -1,4 +1,4 @@
// ts-rs owns bindings/index.ts and rewrites it on export, so this hand-written
// entry point is where the generated files come together.
export * from "./bindings/gen_rpc";
// ts-rs owns bindings/index.ts and rewrites it on export. What remains here
// after the RPC schema moved to @yaakapp-internal/rpc-schema is the
// desktop-only surface: updater and notification types.
export * from "./bindings/index";
@@ -2,7 +2,6 @@ use crate::error::{Error, Result};
use chrono::Utc;
use log::{debug, error, warn};
use notify::Watcher;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;
@@ -10,18 +9,11 @@ use tauri::{AppHandle, Listener, Runtime};
use tokio::select;
use tokio::sync::watch;
use tokio::time::sleep;
use ts_rs::TS;
use yaak_git::{GitWorktreeStatus, git_path_is_ignored, git_repository_paths, git_worktree_status};
use yaak_rpc_schema::GitWatchResult;
const GIT_STATUS_COALESCE_WINDOW: Duration = Duration::from_millis(250);
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "index.ts")]
pub(crate) struct GitWatchResult {
unlisten_event: String,
}
pub(crate) async fn watch_git_worktree_status<R, F>(
app_handle: AppHandle<R>,
dir: &Path,
+1 -17
View File
@@ -8,7 +8,6 @@ use crate::import::{import_data, import_url};
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_json_value, render_template};
use crate::rpc_ext::EphemeralHttpResponse;
use crate::updates::{UpdateMode, UpdateTrigger, YaakUpdater};
use crate::uri_scheme::handle_deep_link;
use error::Result as YaakResult;
@@ -57,6 +56,7 @@ use yaak_plugins::events::{
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_meta::{PluginMetadata, get_plugin_meta};
use yaak_plugins::template_callback::PluginTemplateCallback;
use yaak_rpc_schema::{AppMetaData, EphemeralHttpResponse};
use yaak_sse::sse::ServerSentEvent;
use yaak_tauri_utils::window::WorkspaceWindowTrait;
use yaak_templates::format_json::format_json;
@@ -184,22 +184,6 @@ impl<R: Runtime> PluginContextExt<R> for WebviewWindow<R> {
}
}
#[derive(serde::Serialize, ts_rs::TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")]
pub struct AppMetaData {
is_dev: bool,
version: String,
cli_version: Option<String>,
name: String,
app_data_dir: String,
app_log_dir: String,
vendored_plugin_dir: String,
default_project_dir: String,
feature_updater: bool,
feature_license: bool,
}
async fn cmd_metadata<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<AppMetaData> {
let app_data_dir = app_handle.path().app_data_dir()?;
let app_log_dir = app_handle.path().app_log_dir()?;
File diff suppressed because it is too large Load Diff
+1 -9
View File
@@ -6,11 +6,10 @@ use crate::error::Result;
use crate::models_ext::{BlobManagerExt, QueryManagerExt};
use chrono::Utc;
use log::warn;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tauri::{AppHandle, Listener, Runtime};
use tokio::sync::watch;
use ts_rs::TS;
use yaak_rpc_schema::WatchResult;
use yaak_sync::error::Error::InvalidSyncDirectory;
use yaak_sync::sync::{
FsCandidate, SyncOp, apply_sync_ops, apply_sync_state_ops, compute_sync_ops, get_db_candidates,
@@ -57,13 +56,6 @@ pub(crate) async fn cmd_sync_apply<R: Runtime>(
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "index.ts")]
pub(crate) struct WatchResult {
unlisten_event: String,
}
pub(crate) async fn sync_watch<R, F>(
app_handle: AppHandle<R>,
sync_dir: &Path,
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "yaak-rpc-schema"
version = "0.0.0"
edition = "2024"
authors = ["Gregory Schier"]
publish = false
[dependencies]
serde = { workspace = true, features = ["derive"] }
ts-rs = { workspace = true }
yaak-git = { workspace = true }
yaak-grpc = { workspace = true }
yaak-models = { workspace = true }
yaak-plugins = { workspace = true }
yaak-sse = { workspace = true }
yaak-sync = { workspace = true }
yaak-templates = { workspace = true }
yaak-ws = { workspace = true }
+44
View File
@@ -0,0 +1,44 @@
# yaak-rpc-schema
The wire schema for the app's RPC surface: every command name, its request
payload, and its response type, declared once.
Every host that serves the Yaak UI — the desktop app today, the browser bridge
and anything after it — imports these types and implements the commands against
them. That is what keeps a request's shape from drifting between hosts, and it
is why the TypeScript bindings (`bindings/gen_rpc.ts`, exposed to the frontend
as `@yaakapp-internal/rpc-schema`) are generated from one place.
Nothing here depends on Tauri or on any host. Request structs are plain data,
and so are the few response types declared here rather than in an engine crate.
Command *bodies* live with the host that runs them.
## Adding a command
1. Add its request struct and an entry in `with_commands!` in `src/lib.rs`.
2. Write the adapter in each host — the desktop's live in
`crates-tauri/yaak-app-client/src/rpc_ext.rs`. A host that does not support
the command still has to say so; a missing adapter fails to compile.
3. Regenerate the bindings: `cargo test -p yaak-rpc-schema` writes
`bindings/gen_rpc.ts`, which is committed.
## How hosts consume the list
`with_commands!` takes the name of a `macro_rules!` macro and calls it with the
full `name(Req) -> Res` list. Each host writes a small macro that receives that
list and builds its router:
```rust
macro_rules! register_commands {
( $( $name:ident ( $req:ty ) -> $res:ty ),* $(,)? ) => {
pub fn build_router() -> RpcRouter<MyCtx> {
let mut router = RpcRouter::new();
$( router.register(stringify!($name), rpc_handler_async!($name)); )*
router
}
};
}
yaak_rpc_schema::with_commands!(register_commands);
```
The schema decides *what* commands exist; the host decides *how* each one runs.
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
// The RPC wire schema, generated by ts-rs from the Rust declarations in
// src/lib.rs. `RpcSchema` maps every command name to its (request, response)
// pair; the app's `rpc()` helper derives its command union from it.
export * from "./bindings/gen_rpc";
@@ -0,0 +1,6 @@
{
"name": "@yaakapp-internal/rpc-schema",
"version": "1.0.0",
"private": true,
"main": "index.ts"
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,5 +1,5 @@
import { platform } from "@yaakapp-internal/platform";
import type { WatchResult } from "@yaakapp-internal/tauri-client";
import type { WatchResult } from "@yaakapp-internal/rpc-schema";
import { SyncOp } from "./bindings/gen_sync";
import { WatchEvent } from "./bindings/gen_watch";
+1
View File
@@ -58,6 +58,7 @@
"crates-tauri/yaak-fonts",
"crates-tauri/yaak-license",
"crates-tauri/yaak-mac-window",
"crates/common/yaak-rpc-schema",
"crates/yaak-crypto",
"crates/yaak-git",
"crates/yaak-models",