diff --git a/Cargo.lock b/Cargo.lock index 7ca05257..551f9cf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11034,6 +11034,7 @@ dependencies = [ "yaak-mac-window", "yaak-models", "yaak-plugins", + "yaak-rpc", "yaak-sse", "yaak-sync", "yaak-system-appearance", @@ -11201,6 +11202,7 @@ dependencies = [ "tokio-stream", "tonic", "tonic-reflection", + "ts-rs", "uuid", "yaak-common", "yaak-tls", diff --git a/apps/yaak-client/components/CommercialUseBanner.tsx b/apps/yaak-client/components/CommercialUseBanner.tsx index 6d968900..a69283d8 100644 --- a/apps/yaak-client/components/CommercialUseBanner.tsx +++ b/apps/yaak-client/components/CommercialUseBanner.tsx @@ -1,10 +1,9 @@ -import type { LicenseCheckStatus } from "@yaakapp-internal/license"; +import { checkLicense } from "@yaakapp-internal/license"; import { useCallback, useEffect, useRef, useState } from "react"; import { useKeyValue } from "../hooks/useKeyValue"; import { appInfo } from "../lib/appInfo"; import { pricingUrl } from "../lib/pricingUrl"; import { DismissibleBanner } from "./core/DismissibleBanner"; -import { rpc } from "../lib/rpc"; import { platform } from "@yaakapp-internal/platform"; const COMMERCIAL_USE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000; @@ -95,7 +94,7 @@ async function shouldShowCommercialUsePrompt(): Promise { } try { - const license = await rpc("plugin:yaak-license|check"); + const license = await checkLicense(); return license.status === "personal_use"; } catch (err) { console.log("Failed to check license before commercial-use prompt", err); diff --git a/apps/yaak-client/lib/rpc.ts b/apps/yaak-client/lib/rpc.ts index 22522bfc..74be4d49 100644 --- a/apps/yaak-client/lib/rpc.ts +++ b/apps/yaak-client/lib/rpc.ts @@ -1,73 +1,17 @@ import type { RpcPayload } from "@yaakapp-internal/platform"; import { platform } from "@yaakapp-internal/platform"; +import type { RpcSchema } from "@yaakapp-internal/tauri-client"; /** - * Every backend command the client sends. + * Every backend command the app can call: the generated wire schema, one field + * per `RpcRouter` registration on the Rust side. A typo'd or unregistered + * command name is a compile error. Host plugin commands (license, fonts, + * mac-window) don't appear here — each lives behind its own package's facade. * - * Listing them keeps typos out and gives us the inventory to check the Rust - * side against. Once the app's commands move onto `RpcRouter`, this union is - * replaced by the generated `RpcSchema` and the payload and result types come - * with it, the way `apps/yaak-proxy/lib/rpc.ts` already works. + * `RpcSchema` also carries each command's request and response payload types; + * adopting them at call sites is an incremental follow-up. */ -type AppCmd = - | "cmd_call_grpc_request_action" - | "cmd_call_http_authentication_action" - | "cmd_call_http_request_action" - | "cmd_call_websocket_request_action" - | "cmd_call_workspace_action" - | "cmd_call_folder_action" - | "cmd_check_for_updates" - | "cmd_curl_to_request" - | "cmd_decrypt_template" - | "cmd_default_headers" - | "cmd_delete_all_grpc_connections" - | "cmd_delete_all_http_responses" - | "cmd_delete_send_history" - | "cmd_dismiss_notification" - | "cmd_export_data" - | "cmd_format_graphql" - | "cmd_format_json" - | "cmd_get_http_authentication_config" - | "cmd_get_http_authentication_summaries" - | "cmd_get_http_response_events" - | "cmd_get_sse_events" - | "cmd_get_themes" - | "cmd_get_workspace_meta" - | "cmd_git_add_credential" - | "cmd_git_clone" - | "cmd_grpc_go" - | "cmd_grpc_reflect" - | "cmd_grpc_request_actions" - | "cmd_http_request_actions" - | "cmd_websocket_request_actions" - | "cmd_workspace_actions" - | "cmd_folder_actions" - | "cmd_http_request_body" - | "cmd_http_response_body" - | "cmd_import_data" - | "cmd_metadata" - | "cmd_restart" - | "cmd_new_child_window" - | "cmd_new_main_window" - | "cmd_plugin_info" - | "cmd_plugin_init_errors" - | "cmd_reload_plugins" - | "cmd_render_template" - | "cmd_save_base64_to_binary" - | "cmd_save_response" - | "cmd_secure_template" - | "cmd_send_ephemeral_request" - | "cmd_send_feedback" - | "cmd_send_http_request" - | "cmd_template_function_summaries" - | "cmd_template_function_config" - | "cmd_template_tokens_to_string" - | "models_get_graphql_introspection" - | "models_get_settings" - | "models_grpc_events" - | "models_upsert_graphql_introspection" - | "models_websocket_events" - | "plugin:yaak-license|check"; +type AppCmd = keyof RpcSchema; /** Call a backend command. */ export function rpc(cmd: AppCmd, payload?: RpcPayload): Promise { diff --git a/crates-proxy/yaak-proxy-lib/src/lib.rs b/crates-proxy/yaak-proxy-lib/src/lib.rs index af72a6b6..f8cace44 100644 --- a/crates-proxy/yaak-proxy-lib/src/lib.rs +++ b/crates-proxy/yaak-proxy-lib/src/lib.rs @@ -9,7 +9,7 @@ use log::warn; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::Path; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use ts_rs::TS; use yaak_database::{ModelChangeEvent, UpdateSource}; use yaak_proxy::{CapturedRequest, ProxyEvent, ProxyHandle, RequestState}; @@ -17,15 +17,17 @@ use yaak_rpc::{RpcError, RpcEventEmitter, define_rpc}; // -- Context -- +// Cloned once per dispatched command, so shared state lives behind an `Arc`. +#[derive(Clone)] pub struct ProxyCtx { - handle: Mutex>, + handle: Arc>>, pub db: ProxyQueryManager, pub events: RpcEventEmitter, } impl ProxyCtx { pub fn new(db_path: &Path, events: RpcEventEmitter) -> Self { - Self { handle: Mutex::new(None), db: ProxyQueryManager::new(db_path), events } + Self { handle: Arc::new(Mutex::new(None)), db: ProxyQueryManager::new(db_path), events } } } diff --git a/crates-tauri/yaak-app-client/Cargo.toml b/crates-tauri/yaak-app-client/Cargo.toml index 485dc520..99faaa07 100644 --- a/crates-tauri/yaak-app-client/Cargo.toml +++ b/crates-tauri/yaak-app-client/Cargo.toml @@ -72,6 +72,7 @@ tokio-tungstenite = { version = "0.26.2", default-features = false } url = "2" tokio-util = { version = "0.7", features = ["codec"] } ts-rs = { workspace = true } +yaak-rpc = { workspace = true } uuid = "1.12.1" yaak-api = { workspace = true } yaak-common = { workspace = true } diff --git a/crates-tauri/yaak-app-client/bindings/gen_api.ts b/crates-tauri/yaak-app-client/bindings/gen_api.ts new file mode 100644 index 00000000..9dcbb820 --- /dev/null +++ b/crates-tauri/yaak-app-client/bindings/gen_api.ts @@ -0,0 +1,8 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { PluginVersion } from "./gen_search"; + +export type PluginNameVersion = { name: string, version: string, }; + +export type PluginSearchResponse = { plugins: Array, }; + +export type PluginUpdatesResponse = { plugins: Array, }; diff --git a/crates-tauri/yaak-app-client/bindings/gen_events.ts b/crates-tauri/yaak-app-client/bindings/gen_events.ts new file mode 100644 index 00000000..fdf45142 --- /dev/null +++ b/crates-tauri/yaak-app-client/bindings/gen_events.ts @@ -0,0 +1,389 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models"; + +export type CallFolderActionArgs = { folder: Folder, }; + +export type CallFolderActionRequest = { index: number, pluginRefId: string, args: CallFolderActionArgs, }; + +export type CallGrpcRequestActionArgs = { grpcRequest: GrpcRequest, protoFiles: Array, }; + +export type CallGrpcRequestActionRequest = { index: number, pluginRefId: string, args: CallGrpcRequestActionArgs, }; + +export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, }; + +export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, }; + +export type CallWebsocketRequestActionArgs = { websocketRequest: WebsocketRequest, }; + +export type CallWebsocketRequestActionRequest = { index: number, pluginRefId: string, args: CallWebsocketRequestActionArgs, }; + +export type CallWorkspaceActionArgs = { workspace: Workspace, }; + +export type CallWorkspaceActionRequest = { index: number, pluginRefId: string, args: CallWorkspaceActionArgs, }; + +export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger"; + +export type CompletionOptionType = "constant" | "variable"; + +export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown" | "c" | "clojure" | "csharp" | "go" | "http" | "java" | "kotlin" | "objective_c" | "ocaml" | "php" | "powershell" | "python" | "r" | "ruby" | "shell" | "swift"; + +export type FileFilter = { name: string, +/** + * File extensions to require + */ +extensions: Array, }; + +export type FilterResponse = { content: string, error?: string, }; + +export type FolderAction = { label: string, icon?: Icon, }; + +export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "h_stack" } & FormInputHStack | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown | { "type": "key_value" } & FormInputKeyValue; + +export type FormInputAccordion = { label: string, inputs?: Array, hidden?: boolean, }; + +export type FormInputBanner = { inputs?: Array, hidden?: boolean, color?: Color, }; + +export type FormInputCheckbox = { +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputEditor = { +/** + * Placeholder for the text input + */ +placeholder?: string | null, +/** + * Don't show the editor gutter (line numbers, folds, etc.) + */ +hideGutter?: boolean, +/** + * Language for syntax highlighting + */ +language?: EditorLanguage, readOnly?: boolean, +/** + * Fixed number of visible rows + */ +rows?: number, completionOptions?: Array, +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputFile = { +/** + * The title of the file selection window + */ +title: string, +/** + * Allow selecting multiple files + */ +multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array, +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputHStack = { inputs?: Array, hidden?: boolean, }; + +export type FormInputHttpRequest = { +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputKeyValue = { +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputMarkdown = { content: string, hidden?: boolean, }; + +export type FormInputSelect = { +/** + * The options that will be available in the select input + */ +options: Array, +/** + * The name of the input. The value will be stored at this object attribute in the resulting data + */ +name: string, +/** + * Whether this input is visible for the given configuration. Use this to + * make branching forms. + */ +hidden?: boolean, +/** + * Whether the user must fill in the argument + */ +optional?: boolean, +/** + * The label of the input + */ +label?: string, +/** + * Visually hide the label of the input + */ +hideLabel?: boolean, +/** + * The default value + */ +defaultValue?: string, disabled?: boolean, +/** + * Longer description of the input, likely shown in a tooltip + */ +description?: string, }; + +export type FormInputSelectOption = { label: string, value: string, }; + +export type FormInputText = { +/** + * Placeholder for the text input + */ +placeholder?: string | null, +/** + * Placeholder for the text input + */ +password?: boolean, +/** + * Whether to allow newlines in the input, like a