refactor(core): serialize response once, closes #5641 (#10641)

* refactor(core): serialize response once closes #5641

This change impacts both the custom protocol and the postMessage based IPC implementations. Basically it changes the whole IPC mechanism to work on raw JSON strings so we do not need to serialize a serde_json::Value after serializing to it from a user-provided type.

i benchmarked this with a 150MB file response (returning Vec<u8> instead of tauri::ipc::Response since the latter does not serialize at all) and it went from 29s to 23s (custom protocol) and from 54s to 48s (post message) on macOS.

* fix mobile & lint

* clippy
This commit is contained in:
Lucas Fernandes Nogueira 2024-08-15 16:15:11 -03:00 committed by GitHub
parent d1ee3f4b55
commit d0510f52eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 343 additions and 136 deletions

View File

@ -0,0 +1,6 @@
---
"tauri": patch:breaking
---
Added a dedicated type for IPC response body `InvokeResponseBody` for performance reasons.
This is only a breaking change if you are directly using types from `tauri::ipc`.

View File

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
"type": "object",
"properties": {
"$schema": {

View File

@ -46,8 +46,8 @@ use wry::WebViewBuilderExtWindows;
use tao::{
dpi::{
LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize,
LogicalUnit as ToaLogicalUnit, PhysicalPosition as TaoPhysicalPosition,
PhysicalSize as TaoPhysicalSize, Position as TaoPosition, Size as TaoSize,
PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize,
Position as TaoPosition, Size as TaoSize,
},
event::{Event, StartCause, WindowEvent as TaoWindowEvent},
event_loop::{
@ -793,16 +793,16 @@ impl WindowBuilder for WindowBuilderWrapper {
let mut constraints = WindowSizeConstraints::default();
if let Some(min_width) = config.min_width {
constraints.min_width = Some(ToaLogicalUnit::new(min_width).into());
constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into());
}
if let Some(min_height) = config.min_height {
constraints.min_height = Some(ToaLogicalUnit::new(min_height).into());
constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into());
}
if let Some(max_width) = config.max_width {
constraints.max_width = Some(ToaLogicalUnit::new(max_width).into());
constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into());
}
if let Some(max_height) = config.max_height {
constraints.max_height = Some(ToaLogicalUnit::new(max_height).into());
constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into());
}
window = window.inner_size_constraints(constraints);

View File

@ -2170,7 +2170,9 @@ where
/// - [`bundle`](#bundleconfig): The bundle configurations
/// - [`plugins`](#pluginconfig): The plugins configuration
///
/// ```json title="Example tauri.config.json file"
/// Example tauri.config.json file:
///
/// ```json
/// {
/// "productName": "tauri-app",
/// "version": "0.1.0",

View File

@ -1741,6 +1741,13 @@ tauri::Builder::default()
self.invoke_key,
));
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd"
))]
let app_id = if manager.config.app.enable_gtk_app_id {
Some(manager.config.identifier.clone())
} else {

View File

@ -20,7 +20,7 @@ use crate::{
Manager, Runtime, State, Webview,
};
use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response};
use super::{CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response};
pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:";
pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__";
@ -33,13 +33,13 @@ static CHANNEL_DATA_COUNTER: AtomicU32 = AtomicU32::new(0);
/// Maps a channel id to a pending data that must be send to the JavaScript side via the IPC.
#[derive(Default, Clone)]
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeBody>>>);
pub struct ChannelDataIpcQueue(pub(crate) Arc<Mutex<HashMap<u32, InvokeResponseBody>>>);
/// An IPC channel.
#[derive(Clone)]
pub struct Channel<TSend = InvokeBody> {
pub struct Channel<TSend = InvokeResponseBody> {
id: u32,
on_message: Arc<dyn Fn(InvokeBody) -> crate::Result<()> + Send + Sync>,
on_message: Arc<dyn Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync>,
phantom: std::marker::PhantomData<TSend>,
}
@ -138,13 +138,13 @@ impl<'de> Deserialize<'de> for JavaScriptChannelId {
impl<TSend> Channel<TSend> {
/// Creates a new channel with the given message handler.
pub fn new<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
pub fn new<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
on_message: F,
) -> Self {
Self::new_with_id(CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), on_message)
}
fn new_with_id<F: Fn(InvokeBody) -> crate::Result<()> + Send + Sync + 'static>(
fn new_with_id<F: Fn(InvokeResponseBody) -> crate::Result<()> + Send + Sync + 'static>(
id: u32,
on_message: F,
) -> Self {
@ -195,8 +195,7 @@ impl<TSend> Channel<TSend> {
where
TSend: IpcResponse,
{
let body = data.body()?;
(self.on_message)(body)
(self.on_message)(data.body()?)
}
}

View File

@ -183,7 +183,7 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> {
#[doc(hidden)]
pub mod private {
use crate::{
ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse},
ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse},
Runtime,
};
use futures_util::{FutureExt, TryFutureExt};
@ -220,7 +220,10 @@ pub mod private {
}
#[inline(always)]
pub fn future<T>(self, value: T) -> impl Future<Output = Result<InvokeBody, InvokeError>>
pub fn future<T>(
self,
value: T,
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
where
T: IpcResponse,
{
@ -261,7 +264,7 @@ pub mod private {
pub fn future<T, E>(
self,
value: Result<T, E>,
) -> impl Future<Output = Result<InvokeBody, InvokeError>>
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
where
T: IpcResponse,
E: Into<InvokeError>,
@ -288,7 +291,10 @@ pub mod private {
impl FutureTag {
#[inline(always)]
pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
pub fn future<T, F>(
self,
value: F,
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
where
T: IpcResponse,
F: Future<Output = T> + Send + 'static,
@ -315,7 +321,10 @@ pub mod private {
impl ResultFutureTag {
#[inline(always)]
pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<InvokeBody, InvokeError>>
pub fn future<T, E, F>(
self,
value: F,
) -> impl Future<Output = Result<InvokeResponseBody, InvokeError>>
where
T: IpcResponse,
E: Into<InvokeError>,

View File

@ -40,14 +40,14 @@ const MIN_JSON_PARSE_LEN: usize = 10_240;
/// 1. `serde_json`'s ability to correctly escape and format json into a string.
/// 2. JavaScript engines not accepting anything except another unescaped, literal single quote
/// character to end a string that was opened with it.
fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
value: &T,
fn serialize_js_with<F: FnOnce(&str) -> String>(
json_string: String,
options: serialize_to_javascript::Options,
cb: F,
) -> crate::Result<String> {
// get a raw &str representation of a serialized json value.
let string = serde_json::to_string(value)?;
let raw = RawValue::from_string(string)?;
let raw = RawValue::from_string(json_string)?;
// from here we know json.len() > 1 because an empty string is not a valid json value.
let json = raw.get();
@ -77,14 +77,21 @@ fn serialize_js_with<T: Serialize, F: FnOnce(&str) -> String>(
Ok(return_val)
}
/// Formats a function name and argument to be evaluated as callback.
/// Formats a function name and a serializable argument to be evaluated as callback.
///
/// See [`format_raw`] for more information.
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
format_raw(function_name, serde_json::to_string(arg)?)
}
/// Formats a function name and a raw JSON string argument to be evaluated as callback.
///
/// This will serialize primitive JSON types (e.g. booleans, strings, numbers, etc.) as JavaScript literals,
/// but will serialize arrays and objects whose serialized JSON string is smaller than 1 GB and larger
/// than 10 KiB with `JSON.parse('...')`.
/// See [json-parse-benchmark](https://github.com/GoogleChromeLabs/json-parse-benchmark).
pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result<String> {
serialize_js_with(arg, Default::default(), |arg| {
pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result<String> {
serialize_js_with(json_string, Default::default(), |arg| {
format!(
r#"
if (window["_{fn}"]) {{
@ -97,7 +104,21 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
})
}
/// Formats a Result type to its Promise response.
/// Formats a serializable Result type to its Promise response.
///
/// See [`format_result_raw`] for more information.
pub fn format_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::Result<String> {
match result {
Ok(res) => format(success_callback, &res),
Err(err) => format(error_callback, &err),
}
}
/// Formats a Result type of raw JSON strings to its Promise response.
/// Useful for Promises handling.
/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
@ -107,14 +128,14 @@ pub fn format<T: Serialize>(function_name: CallbackFn, arg: &T) -> crate::Result
/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise.
///
/// Note that the callback strings are automatically generated by the `invoke` helper.
pub fn format_result<T: Serialize, E: Serialize>(
result: Result<T, E>,
pub fn format_result_raw(
raw_result: Result<String, String>,
success_callback: CallbackFn,
error_callback: CallbackFn,
) -> crate::Result<String> {
match result {
Ok(res) => format(success_callback, &res),
Err(err) => format(error_callback, &err),
match raw_result {
Ok(res) => format_raw(success_callback, res),
Err(err) => format_raw(error_callback, err),
}
}
@ -130,8 +151,31 @@ mod test {
}
}
#[derive(Debug, Clone)]
struct JsonStr(String);
impl Arbitrary for JsonStr {
fn arbitrary(g: &mut Gen) -> Self {
if bool::arbitrary(g) {
Self(format!(
"{{ {}: {} }}",
serde_json::to_string(&String::arbitrary(g)).unwrap(),
serde_json::to_string(&String::arbitrary(g)).unwrap()
))
} else {
Self(serde_json::to_string(&String::arbitrary(g)).unwrap())
}
}
}
fn serialize_js<T: Serialize>(value: &T) -> crate::Result<String> {
serialize_js_with(value, Default::default(), |v| v.into())
serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| {
v.into()
})
}
fn serialize_js_raw(value: impl Into<String>) -> crate::Result<String> {
serialize_js_with(value.into(), Default::default(), |v| v.into())
}
#[test]
@ -213,4 +257,79 @@ mod test {
serde_json::Value::String(value),
))
}
#[test]
fn test_serialize_js_raw() {
assert_eq!(serialize_js_raw("null").unwrap(), "null");
assert_eq!(serialize_js_raw("5").unwrap(), "5");
assert_eq!(
serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(),
"{ \"x\": [1, 2, 3] }"
);
#[derive(serde::Serialize)]
struct JsonObj {
value: String,
}
let raw_str = "T".repeat(MIN_JSON_PARSE_LEN);
assert_eq!(
serialize_js_raw(format!("\"{raw_str}\"")).unwrap(),
format!("\"{raw_str}\"")
);
assert_eq!(
serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(),
format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')")
);
assert_eq!(
serialize_js(&JsonObj {
value: format!("\"{raw_str}\"")
})
.unwrap(),
format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')")
);
let dangerous_json = RawValue::from_string(
r#"{"test":"don\\🚀🐱‍👤\\'t forget to escape me!🚀🐱‍👤","te🚀🐱👤st2":"don't forget to escape me!","test3":"\\🚀🐱‍👤\\\\'''\\\\🚀🐱‍👤\\\\🚀🐱‍👤\\'''''"}"#.into()
).unwrap();
let definitely_escaped_dangerous_json = format!(
"JSON.parse('{}')",
dangerous_json
.get()
.replace('\\', "\\\\")
.replace('\'', "\\'")
);
let escape_single_quoted_json_test =
serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string();
let result = r#"JSON.parse('{"test":"don\\\\🚀🐱‍👤\\\\\'t forget to escape me!🚀🐱‍👤","te🚀🐱👤st2":"don\'t forget to escape me!","test3":"\\\\🚀🐱‍👤\\\\\\\\\'\'\'\\\\\\\\🚀🐱‍👤\\\\\\\\🚀🐱‍👤\\\\\'\'\'\'\'"}')"#;
assert_eq!(definitely_escaped_dangerous_json, result);
assert_eq!(escape_single_quoted_json_test, result);
}
// check arbitrary strings in the format callback function
#[quickcheck]
fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool {
let a = a.0;
// call format callback
let fc = format_raw(f, a.clone()).unwrap();
fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a))
|| fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a))
}
// check arbitrary strings in format_result
#[quickcheck]
fn qc_format_raw_res(result: Result<JsonStr, JsonStr>, c: CallbackFn, ec: CallbackFn) -> bool {
let result = result.map(|v| v.0).map_err(|e| e.0);
let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result");
let (function, value) = match result {
Ok(v) => (c, v),
Err(e) => (ec, e),
};
resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value))
}
}

View File

@ -72,14 +72,8 @@ impl From<Vec<u8>> for InvokeBody {
}
}
impl IpcResponse for InvokeBody {
fn body(self) -> crate::Result<InvokeBody> {
Ok(self)
}
}
impl InvokeBody {
#[allow(dead_code)]
#[cfg(mobile)]
pub(crate) fn into_json(self) -> JsonValue {
match self {
Self::Json(v) => v,
@ -88,12 +82,51 @@ impl InvokeBody {
}
}
}
}
/// Attempts to deserialize the invoke body.
/// Possible values of an IPC response.
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum InvokeResponseBody {
/// Json payload.
Json(String),
/// Bytes payload.
Raw(Vec<u8>),
}
impl From<String> for InvokeResponseBody {
fn from(value: String) -> Self {
Self::Json(value)
}
}
impl From<Vec<u8>> for InvokeResponseBody {
fn from(value: Vec<u8>) -> Self {
Self::Raw(value)
}
}
impl From<InvokeBody> for InvokeResponseBody {
fn from(value: InvokeBody) -> Self {
match value {
InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()),
InvokeBody::Raw(v) => Self::Raw(v),
}
}
}
impl IpcResponse for InvokeResponseBody {
fn body(self) -> crate::Result<InvokeResponseBody> {
Ok(self)
}
}
impl InvokeResponseBody {
/// Attempts to deserialize the response.
pub fn deserialize<T: DeserializeOwned>(self) -> serde_json::Result<T> {
match self {
InvokeBody::Json(v) => serde_json::from_value(v),
InvokeBody::Raw(v) => T::deserialize(v.into_deserializer()),
Self::Json(v) => serde_json::from_str(&v),
Self::Raw(v) => T::deserialize(v.into_deserializer()),
}
}
}
@ -130,12 +163,12 @@ impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> {
/// Marks a type as a response to an IPC call.
pub trait IpcResponse {
/// Resolve the IPC response body.
fn body(self) -> crate::Result<InvokeBody>;
fn body(self) -> crate::Result<InvokeResponseBody>;
}
impl<T: Serialize> IpcResponse for T {
fn body(self) -> crate::Result<InvokeBody> {
serde_json::to_value(self)
fn body(self) -> crate::Result<InvokeResponseBody> {
serde_json::to_string(&self)
.map(Into::into)
.map_err(Into::into)
}
@ -143,18 +176,18 @@ impl<T: Serialize> IpcResponse for T {
/// The IPC request.
pub struct Response {
body: InvokeBody,
body: InvokeResponseBody,
}
impl IpcResponse for Response {
fn body(self) -> crate::Result<InvokeBody> {
fn body(self) -> crate::Result<InvokeResponseBody> {
Ok(self.body)
}
}
impl Response {
/// Defines a response with the given body.
pub fn new(body: impl Into<InvokeBody>) -> Self {
pub fn new(body: impl Into<InvokeResponseBody>) -> Self {
Self { body: body.into() }
}
}
@ -177,19 +210,19 @@ pub struct Invoke<R: Runtime> {
/// Error response from an [`InvokeMessage`].
#[derive(Debug)]
pub struct InvokeError(pub JsonValue);
pub struct InvokeError(pub serde_json::Value);
impl InvokeError {
/// Create an [`InvokeError`] as a string of the [`std::error::Error`] message.
#[inline(always)]
pub fn from_error<E: std::error::Error>(error: E) -> Self {
Self(JsonValue::String(error.to_string()))
Self(serde_json::Value::String(error.to_string()))
}
/// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message.
#[inline(always)]
pub fn from_anyhow(error: anyhow::Error) -> Self {
Self(JsonValue::String(format!("{error:#}")))
Self(serde_json::Value::String(format!("{error:#}")))
}
}
@ -205,7 +238,7 @@ impl<T: Serialize> From<T> for InvokeError {
impl From<crate::Error> for InvokeError {
#[inline(always)]
fn from(error: crate::Error) -> Self {
Self(JsonValue::String(error.to_string()))
Self(serde_json::Value::String(error.to_string()))
}
}
@ -213,24 +246,11 @@ impl From<crate::Error> for InvokeError {
#[derive(Debug)]
pub enum InvokeResponse {
/// Resolve the promise.
Ok(InvokeBody),
Ok(InvokeResponseBody),
/// Reject the promise.
Err(InvokeError),
}
impl Serialize for InvokeResponse {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Ok(InvokeBody::Json(j)) => j.serialize(serializer),
Self::Ok(InvokeBody::Raw(b)) => b.serialize(serializer),
Self::Err(e) => e.0.serialize(serializer),
}
}
}
impl<T: IpcResponse, E: Into<InvokeError>> From<Result<T, E>> for InvokeResponse {
#[inline]
fn from(result: Result<T, E>) -> Self {
@ -311,7 +331,7 @@ impl<R: Runtime> InvokeResolver<R> {
/// Reply to the invoke promise with an async task which is already serialized.
pub fn respond_async_serialized<F>(self, task: F)
where
F: Future<Output = Result<InvokeBody, InvokeError>> + Send + 'static,
F: Future<Output = Result<InvokeResponseBody, InvokeError>> + Send + 'static,
{
crate::async_runtime::spawn(async move {
let response = match task.await {
@ -531,22 +551,18 @@ mod tests {
use super::*;
#[test]
fn deserialize_invoke_body() {
let json = InvokeBody::Json(serde_json::Value::Array(vec![
serde_json::Value::Number(1.into()),
serde_json::Value::Number(123.into()),
serde_json::Value::Number(1231.into()),
]));
fn deserialize_invoke_response_body() {
let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string());
assert_eq!(json.deserialize::<Vec<u16>>().unwrap(), vec![1, 123, 1231]);
let json = InvokeBody::Json(serde_json::Value::String("string value".into()));
let json = InvokeResponseBody::Json("\"string value\"".to_string());
assert_eq!(json.deserialize::<String>().unwrap(), "string value");
let json = InvokeBody::Json(serde_json::Value::String("string value".into()));
let json = InvokeResponseBody::Json("\"string value\"".to_string());
assert!(json.deserialize::<Vec<u16>>().is_err());
let values = vec![1, 2, 3, 4, 5, 6, 1];
let raw = InvokeBody::Raw(values.clone());
let raw = InvokeResponseBody::Raw(values.clone());
assert_eq!(raw.deserialize::<Vec<u8>>().unwrap(), values);
}
}

View File

@ -5,6 +5,7 @@
use std::{borrow::Cow, sync::Arc};
use crate::{
ipc::InvokeResponseBody,
manager::AppManager,
webview::{InvokeRequest, UriSchemeProtocolHandler},
Runtime,
@ -18,7 +19,7 @@ use http::{
};
use url::Url;
use super::{CallbackFn, InvokeBody, InvokeResponse};
use super::{CallbackFn, InvokeResponse};
const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback";
const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error";
@ -67,8 +68,8 @@ pub fn get<R: Runtime>(manager: Arc<AppManager<R>>, label: String) -> UriSchemeP
span.record(
"request",
match &request.body {
InvokeBody::Json(j) => serde_json::to_string(j).unwrap(),
InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(),
super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(),
super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(),
},
);
#[cfg(feature = "tracing")]
@ -85,12 +86,26 @@ pub fn get<R: Runtime>(manager: Arc<AppManager<R>>, label: String) -> UriSchemeP
.entered();
#[cfg(feature = "tracing")]
let response_span = tracing::trace_span!(
"ipc::request::response",
response = serde_json::to_string(&response).unwrap(),
mime_type = tracing::field::Empty
)
.entered();
let response_span = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
"ipc::request::response",
response = v,
mime_type = tracing::field::Empty
)
.entered(),
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
"ipc::request::response",
response = format!("{v:?}"),
mime_type = tracing::field::Empty
)
.entered(),
InvokeResponse::Err(e) => tracing::trace_span!(
"ipc::request::response",
error = format!("{e:?}"),
mime_type = tracing::field::Empty
)
.entered(),
};
let response_header = match &response {
InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK,
@ -98,11 +113,11 @@ pub fn get<R: Runtime>(manager: Arc<AppManager<R>>, label: String) -> UriSchemeP
};
let (mut response, mime_type) = match response {
InvokeResponse::Ok(InvokeBody::Json(v)) => (
http::Response::new(serde_json::to_vec(&v).unwrap().into()),
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => (
http::Response::new(v.as_bytes().to_vec().into()),
mime::APPLICATION_JSON,
),
InvokeResponse::Ok(InvokeBody::Raw(v)) => (
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => (
http::Response::new(v.into()),
mime::APPLICATION_OCTET_STREAM,
),
@ -302,14 +317,8 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
webview.on_message(
request,
Box::new(move |webview, cmd, response, callback, error| {
use crate::ipc::{
format_callback::{
format as format_callback, format_result as format_callback_result,
},
Channel,
};
use crate::ipc::Channel;
use crate::sealed::ManagerBase;
use serde_json::Value as JsonValue;
#[cfg(feature = "tracing")]
let _respond_span = tracing::trace_span!(
@ -327,7 +336,7 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
) {
let eval_js = match js {
Ok(js) => js,
Err(e) => format_callback(error, &e.to_string())
Err(e) => crate::ipc::format_callback::format(error, &e.to_string())
.expect("unable to serialize response error string to json"),
};
@ -339,51 +348,80 @@ fn handle_ipc_message<R: Runtime>(request: Request<String>, manager: &AppManager
&& !options.custom_protocol_ipc_blocked;
#[cfg(feature = "tracing")]
let _response_span = tracing::trace_span!(
"ipc::request::response",
response = serde_json::to_string(&response).unwrap(),
mime_type = match &response {
InvokeResponse::Ok(InvokeBody::Json(_)) => mime::APPLICATION_JSON,
InvokeResponse::Ok(InvokeBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM,
InvokeResponse::Err(_) => mime::APPLICATION_JSON,
}
.essence_str()
)
.entered();
let mime_type = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON,
InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM,
InvokeResponse::Err(_) => mime::APPLICATION_JSON,
};
match &response {
InvokeResponse::Ok(InvokeBody::Json(v)) => {
#[cfg(feature = "tracing")]
let _response_span = match &response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!(
"ipc::request::response",
response = v,
mime_type = mime_type.essence_str()
)
.entered(),
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!(
"ipc::request::response",
response = format!("{v:?}"),
mime_type = mime_type.essence_str()
)
.entered(),
InvokeResponse::Err(e) => tracing::trace_span!(
"ipc::request::response",
response = format!("{e:?}"),
mime_type = mime_type.essence_str()
)
.entered(),
};
match response {
InvokeResponse::Ok(InvokeResponseBody::Json(v)) => {
if !(cfg!(target_os = "macos") || cfg!(target_os = "ios"))
&& matches!(v, JsonValue::Object(_) | JsonValue::Array(_))
&& (v.starts_with('{') || v.starts_with('['))
&& can_use_channel_for_response
{
let _ = Channel::from_callback_fn(webview, callback).send(v);
let _ = Channel::from_callback_fn(webview, callback)
.send(InvokeResponseBody::Json(v));
} else {
responder_eval(
&webview,
format_callback_result(Result::<_, ()>::Ok(v), callback, error),
crate::ipc::format_callback::format_result_raw(
Result::<_, String>::Ok(v),
callback,
error,
),
error,
)
}
}
InvokeResponse::Ok(InvokeBody::Raw(v)) => {
InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => {
if cfg!(target_os = "macos")
|| cfg!(target_os = "ios")
|| !can_use_channel_for_response
{
responder_eval(
&webview,
format_callback_result(Result::<_, ()>::Ok(v), callback, error),
crate::ipc::format_callback::format_result(
Result::<_, ()>::Ok(v),
callback,
error,
),
error,
);
} else {
let _ =
Channel::from_callback_fn(webview, callback).send(InvokeBody::Raw(v.clone()));
let _ = Channel::from_callback_fn(webview, callback)
.send(InvokeResponseBody::Raw(v.clone()));
}
}
InvokeResponse::Err(e) => responder_eval(
&webview,
format_callback_result(Result::<(), _>::Err(&e.0), callback, error),
crate::ipc::format_callback::format_result(
Result::<(), _>::Err(&e.0),
callback,
error,
),
error,
),
}
@ -530,7 +568,7 @@ mod tests {
use std::str::FromStr;
use super::*;
use crate::{manager::AppManager, plugin::PluginStore, StateManager, Wry};
use crate::{ipc::InvokeBody, manager::AppManager, plugin::PluginStore, StateManager, Wry};
use http::header::*;
use serde_json::json;
use tauri_macros::generate_context;

View File

@ -615,6 +615,7 @@ impl<R: Runtime> AppManager<R> {
}
}
#[cfg(desktop)]
pub(crate) fn on_webview_close(&self, label: &str) {
self.webview.webviews_lock().remove(label);

View File

@ -228,7 +228,7 @@ impl<T: UserEvent> RuntimeHandle<T> for MockRuntimeHandle {
))
});
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
return unimplemented!();
unimplemented!();
}
fn primary_monitor(&self) -> Option<Monitor> {
@ -725,7 +725,7 @@ impl<T: UserEvent> WindowDispatch<T> for MockWindowDispatcher {
))
};
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
return unimplemented!();
unimplemented!();
}
fn center(&self) -> Result<()> {

View File

@ -58,7 +58,7 @@ use serialize_to_javascript::DefaultTemplate;
use std::{borrow::Cow, collections::HashMap, fmt::Debug};
use crate::{
ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority},
ipc::{InvokeError, InvokeResponse, InvokeResponseBody, RuntimeAuthority},
webview::InvokeRequest,
App, Assets, Builder, Context, Pattern, Runtime, Webview,
};
@ -282,7 +282,7 @@ pub fn assert_ipc_response<
pub fn get_ipc_response<W: AsRef<Webview<MockRuntime>>>(
webview: &W,
request: InvokeRequest,
) -> Result<InvokeBody, serde_json::Value> {
) -> Result<InvokeResponseBody, serde_json::Value> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
webview.as_ref().clone().on_message(
request,

View File

@ -20,7 +20,7 @@ use tauri_runtime::{
};
use tauri_runtime::{
webview::{DetachedWebview, PendingWebview, WebviewAttributes},
Rect, WebviewDispatch,
WebviewDispatch,
};
use tauri_utils::config::{WebviewUrl, WindowConfig};
pub use url::Url;
@ -605,7 +605,7 @@ tauri::Builder::default()
let mut pending = self.into_pending_webview(&window, window.label())?;
pending.webview_attributes.bounds = Some(Rect { size, position });
pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position });
let webview = match &mut window.runtime() {
RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending),
@ -902,7 +902,7 @@ impl<R: Runtime> Webview<R> {
}
/// Resizes this webview.
pub fn set_bounds(&self, bounds: Rect) -> crate::Result<()> {
pub fn set_bounds(&self, bounds: tauri_runtime::Rect) -> crate::Result<()> {
self
.webview
.dispatcher
@ -958,7 +958,7 @@ impl<R: Runtime> Webview<R> {
}
/// Returns the bounds of the webviews's client area.
pub fn bounds(&self) -> crate::Result<Rect> {
pub fn bounds(&self) -> crate::Result<tauri_runtime::Rect> {
self.webview.dispatcher.bounds().map_err(Into::into)
}

View File

@ -27,7 +27,6 @@ use crate::{
},
};
use serde::Serialize;
use tauri_runtime::window::WindowSizeConstraints;
use tauri_utils::config::{WebviewUrl, WindowConfig};
use url::Url;
@ -396,7 +395,10 @@ impl<'a, R: Runtime, M: Manager<R>> WebviewWindowBuilder<'a, R, M> {
/// Window inner size constraints.
#[must_use]
pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self {
pub fn inner_size_constraints(
mut self,
constraints: tauri_runtime::window::WindowSizeConstraints,
) -> Self {
self.window_builder = self.window_builder.inner_size_constraints(constraints);
self
}
@ -1465,7 +1467,10 @@ impl<R: Runtime> WebviewWindow<R> {
}
/// Sets this window's minimum inner width.
pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> {
pub fn set_size_constraints(
&self,
constriants: tauri_runtime::window::WindowSizeConstraints,
) -> crate::Result<()> {
self.webview.window().set_size_constraints(constriants)
}

View File

@ -9,7 +9,6 @@ pub(crate) mod plugin;
use tauri_runtime::{
dpi::{PhysicalPosition, PhysicalSize},
webview::PendingWebview,
window::WindowSizeConstraints,
};
pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState};
@ -461,7 +460,10 @@ impl<'a, R: Runtime, M: Manager<R>> WindowBuilder<'a, R, M> {
/// Window inner size constraints.
#[must_use]
pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self {
pub fn inner_size_constraints(
mut self,
constraints: tauri_runtime::window::WindowSizeConstraints,
) -> Self {
self.window_builder = self.window_builder.inner_size_constraints(constraints);
self
}
@ -1830,7 +1832,10 @@ tauri::Builder::default()
}
/// Sets this window's minimum inner width.
pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> {
pub fn set_size_constraints(
&self,
constriants: tauri_runtime::window::WindowSizeConstraints,
) -> crate::Result<()> {
self
.window
.dispatcher

View File

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Config",
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
"type": "object",
"properties": {
"$schema": {