Эх сурвалжийг харах

feat(core): #[command] return with autoref specialization workaround fix #1672 (#1734)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
chip 4 жил өмнө
parent
commit
bb8dafbe1e

+ 6 - 0
.changes/async-commands.md

@@ -0,0 +1,6 @@
+---
+"tauri": patch
+"tauri-macros": patch
+---
+
+Only commands with a `async fn` are executed on a separate task. `#[command] fn command_name` runs on the main thread.

+ 6 - 0
.changes/command-return.md

@@ -0,0 +1,6 @@
+---
+"tauri": patch
+"tauri-macros": patch
+---
+
+Improves support for commands returning `Result`.

+ 1 - 4
core/tauri-macros/src/command/mod.rs

@@ -5,10 +5,7 @@
 use proc_macro2::Ident;
 use syn::{Path, PathSegment};
 
-pub use self::{
-  handler::Handler,
-  wrapper::{Wrapper, WrapperBody},
-};
+pub use self::{handler::Handler, wrapper::wrapper};
 
 mod handler;
 mod wrapper;

+ 126 - 126
core/tauri-macros/src/command/wrapper.rs

@@ -2,147 +2,147 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use proc_macro2::TokenStream;
-use quote::{quote, ToTokens, TokenStreamExt};
-use std::convert::TryFrom;
-use syn::{spanned::Spanned, FnArg, Ident, ItemFn, Pat, ReturnType, Type, Visibility};
-
-/// The command wrapper created for a function marked with `#[command]`.
-pub struct Wrapper {
-  function: ItemFn,
-  visibility: Visibility,
-  maybe_export: TokenStream,
-  wrapper: Ident,
-  body: syn::Result<WrapperBody>,
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::quote;
+use syn::{
+  parse::{Parse, ParseBuffer},
+  parse_macro_input,
+  spanned::Spanned,
+  FnArg, Ident, ItemFn, Pat, Token, Visibility,
+};
+
+/// The execution context of the command.
+enum ExecutionContext {
+  Async,
+  Blocking,
 }
 
-impl Wrapper {
-  /// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
-  pub fn new(function: ItemFn, body: syn::Result<WrapperBody>) -> Self {
-    // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`
-    let maybe_export = match &function.vis {
-      Visibility::Public(_) => quote!(#[macro_export]),
-      _ => Default::default(),
-    };
-
-    let visibility = function.vis.clone();
-    let wrapper = super::format_command_wrapper(&function.sig.ident);
-
-    Self {
-      function,
-      visibility,
-      maybe_export,
-      wrapper,
-      body,
+impl Parse for ExecutionContext {
+  fn parse(input: &ParseBuffer) -> syn::Result<Self> {
+    if input.is_empty() {
+      return Ok(Self::Blocking);
     }
+
+    input
+      .parse::<Token![async]>()
+      .map(|_| Self::Async)
+      .map_err(|_| {
+        syn::Error::new(
+          input.span(),
+          "only a single item `async` is currently allowed",
+        )
+      })
   }
 }
 
-impl From<Wrapper> for proc_macro::TokenStream {
-  fn from(
-    Wrapper {
-      function,
-      maybe_export,
-      wrapper,
-      body,
-      visibility,
-    }: Wrapper,
-  ) -> Self {
-    // either use the successful body or a `compile_error!` of the error occurred while parsing it.
-    let body = body
-      .as_ref()
-      .map(ToTokens::to_token_stream)
-      .unwrap_or_else(syn::Error::to_compile_error);
-
-    // we `use` the macro so that other modules can resolve the with the same path as the function.
-    // this is dependent on rust 2018 edition.
-    quote!(
-      #function
-      #maybe_export
-      macro_rules! #wrapper { ($path:path, $invoke:ident) => {{ #body }}; }
-      #visibility use #wrapper;
-    )
-    .into()
-  }
+/// Create a new [`Wrapper`] from the function and the generated code parsed from the function.
+pub fn wrapper(attributes: TokenStream, item: TokenStream) -> TokenStream {
+  let function = parse_macro_input!(item as ItemFn);
+  let wrapper = super::format_command_wrapper(&function.sig.ident);
+  let visibility = &function.vis;
+
+  // macros used with `pub use my_macro;` need to be exported with `#[macro_export]`
+  let maybe_macro_export = match &function.vis {
+    Visibility::Public(_) => quote!(#[macro_export]),
+    _ => Default::default(),
+  };
+
+  // body to the command wrapper or a `compile_error!` of an error occurred while parsing it.
+  let body = syn::parse::<ExecutionContext>(attributes)
+    .map(|context| match function.sig.asyncness {
+      Some(_) => ExecutionContext::Async,
+      None => context,
+    })
+    .and_then(|context| match context {
+      ExecutionContext::Async => body_async(&function),
+      ExecutionContext::Blocking => body_blocking(&function),
+    })
+    .unwrap_or_else(syn::Error::into_compile_error);
+
+  // Rely on rust 2018 edition to allow importing a macro from a path.
+  quote!(
+    #function
+
+    #maybe_macro_export
+    macro_rules! #wrapper {
+        // double braces because the item is expected to be a block expression
+        ($path:path, $invoke:ident) => {{
+          // import all the autoref specialization items
+          #[allow(unused_imports)]
+          use ::tauri::command::private::*;
+
+          // prevent warnings when the body is a `compile_error!` or if the command has no arguments
+          #[allow(unused_variables)]
+          let ::tauri::Invoke { message, resolver } = $invoke;
+
+          #body
+      }};
+    }
+
+    // allow the macro to be resolved with the same path as the command function
+    #[allow(unused_imports)]
+    #visibility use #wrapper;
+  )
+  .into()
 }
 
-/// Body of the wrapper that maps the command parameters into callable arguments from [`Invoke`].
+/// Generates an asynchronous command response from the arguments and return value of a function.
 ///
-/// This is possible because we require the command parameters to be [`CommandArg`] and use type
-/// inference to put values generated from that trait into the arguments of the called command.
+/// See the [`tauri::command`] module for all the items and traits that make this possible.
 ///
-/// [`CommandArg`]: https://docs.rs/tauri/*/tauri/command/trait.CommandArg.html
-/// [`Invoke`]: https://docs.rs/tauri/*/tauri/struct.Invoke.html
-pub struct WrapperBody(TokenStream);
-
-impl TryFrom<&ItemFn> for WrapperBody {
-  type Error = syn::Error;
-
-  fn try_from(function: &ItemFn) -> syn::Result<Self> {
-    // the name of the #[command] function is the name of the command to handle
-    let command = function.sig.ident.clone();
-
-    // automatically append await when the #[command] function is async
-    let maybe_await = match function.sig.asyncness {
-      Some(_) => quote!(.await),
-      None => Default::default(),
-    };
-
-    // todo: detect command return types automatically like params, removes parsing type name
-    let returns_result = match function.sig.output {
-      ReturnType::Type(_, ref ty) => match &**ty {
-        Type::Path(type_path) => type_path
-          .path
-          .segments
-          .first()
-          .map(|seg| seg.ident == "Result")
-          .unwrap_or_default(),
-        _ => false,
-      },
-      ReturnType::Default => false,
-    };
-
-    let mut args = Vec::new();
-    for param in &function.sig.inputs {
-      args.push(parse_arg(&command, param)?);
-    }
-
-    // todo: change this to automatically detect result returns (see above result todo)
-    // if the command handler returns a Result,
-    // we just map the values to the ones expected by Tauri
-    // otherwise we wrap it with an `Ok()`, converting the return value to tauri::InvokeResponse
-    // note that all types must implement `serde::Serialize`.
-    let result = if returns_result {
-      quote! {
-        let result = $path(#(#args?),*);
-        ::core::result::Result::Ok(result #maybe_await?)
-      }
-    } else {
-      quote! {
+/// * Requires binding `message` and `resolver`.
+/// * Requires all the traits from `tauri::command::private` to be in scope.
+///
+/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
+fn body_async(function: &ItemFn) -> syn::Result<TokenStream2> {
+  parse_args(function).map(|args| {
+    quote! {
+      resolver.respond_async_serialized(async move {
         let result = $path(#(#args?),*);
-        ::core::result::Result::<_, ::tauri::InvokeError>::Ok(result #maybe_await)
-      }
-    };
-
-    Ok(Self(result))
-  }
+        (&result).async_kind().future(result).await
+      })
+    }
+  })
 }
 
-impl ToTokens for WrapperBody {
-  fn to_tokens(&self, tokens: &mut TokenStream) {
-    let body = &self.0;
+/// Generates a blocking command response from the arguments and return value of a function.
+///
+/// See the [`tauri::command`] module for all the items and traits that make this possible.
+///
+/// * Requires binding `message` and `resolver`.
+/// * Requires all the traits from `tauri::command::private` to be in scope.
+///
+/// [`tauri::command`]: https://docs.rs/tauri/*/tauri/runtime/index.html
+fn body_blocking(function: &ItemFn) -> syn::Result<TokenStream2> {
+  let args = parse_args(function)?;
+
+  // the body of a `match` to early return any argument that wasn't successful in parsing.
+  let match_body = quote!({
+    Ok(arg) => arg,
+    Err(err) => return resolver.invoke_error(err),
+  });
+
+  Ok(quote! {
+    let result = $path(#(match #args #match_body),*);
+    (&result).blocking_kind().block(result, resolver);
+  })
+}
 
-    // we #[allow(unused_variables)] because a command with no arguments will not use message.
-    tokens.append_all(quote!(
-      #[allow(unused_variables)]
-      let ::tauri::Invoke { message, resolver } = $invoke;
-      resolver.respond_async(async move { #body });
-    ))
-  }
+/// Parse all arguments for the command wrapper to use from the signature of the command function.
+fn parse_args(function: &ItemFn) -> syn::Result<Vec<TokenStream2>> {
+  function
+    .sig
+    .inputs
+    .iter()
+    .map(|arg| parse_arg(&function.sig.ident, arg))
+    .collect()
 }
 
-/// Transform a [`FnArg`] into a command argument. Expects borrowable binding `message` to exist.
-fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream> {
+/// Transform a [`FnArg`] into a command argument.
+///
+/// * Requires binding `message`.
+fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream2> {
   // we have no use for self arguments
   let mut arg = match arg {
     FnArg::Typed(arg) => arg.pat.as_ref().clone(),
@@ -154,7 +154,7 @@ fn parse_arg(command: &Ident, arg: &FnArg) -> syn::Result<TokenStream> {
     }
   };
 
-  // we only support patterns supported as arguments to a `ItemFn`.
+  // we only support patterns that allow us to extract some sort of keyed identifier.
   let key = match &mut arg {
     Pat::Ident(arg) => arg.ident.to_string(),
     Pat::Wild(_) => "_".into(),

+ 3 - 6
core/tauri-macros/src/lib.rs

@@ -5,8 +5,7 @@
 extern crate proc_macro;
 use crate::context::ContextItems;
 use proc_macro::TokenStream;
-use std::convert::TryFrom;
-use syn::{parse_macro_input, ItemFn};
+use syn::parse_macro_input;
 
 mod command;
 
@@ -14,10 +13,8 @@ mod command;
 mod context;
 
 #[proc_macro_attribute]
-pub fn command(_attrs: TokenStream, item: TokenStream) -> TokenStream {
-  let function = parse_macro_input!(item as ItemFn);
-  let body = command::WrapperBody::try_from(&function);
-  command::Wrapper::new(function, body).into()
+pub fn command(attributes: TokenStream, item: TokenStream) -> TokenStream {
+  command::wrapper(attributes, item)
 }
 
 #[proc_macro]

+ 145 - 2
core/tauri/src/command.rs

@@ -7,7 +7,7 @@
 use crate::hooks::InvokeError;
 use crate::{InvokeMessage, Params};
 use serde::de::Visitor;
-use serde::Deserializer;
+use serde::{Deserialize, Deserializer};
 
 /// Represents a custom command.
 pub struct CommandItem<'a, P: Params> {
@@ -44,7 +44,7 @@ pub trait CommandArg<'de, P: Params>: Sized {
 }
 
 /// Automatically implement [`CommandArg`] for any type that can be deserialized.
-impl<'de, D: serde::Deserialize<'de>, P: Params> CommandArg<'de, P> for D {
+impl<'de, D: Deserialize<'de>, P: Params> CommandArg<'de, P> for D {
   fn from_command(command: CommandItem<'de, P>) -> Result<Self, InvokeError> {
     let arg = command.key;
     Self::deserialize(command).map_err(|e| crate::Error::InvalidArgs(arg, e).into())
@@ -137,3 +137,146 @@ impl<'de, P: Params> Deserializer<'de> for CommandItem<'de, P> {
   pass!(deserialize_identifier, visitor: V);
   pass!(deserialize_ignored_any, visitor: V);
 }
+
+/// [Autoref-based stable specialization](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md)
+#[doc(hidden)]
+pub mod private {
+  use crate::{InvokeError, InvokeResolver, Params};
+  use futures::{FutureExt, TryFutureExt};
+  use serde::Serialize;
+  use serde_json::Value;
+  use std::future::Future;
+
+  // ===== impl Serialize =====
+
+  pub struct SerializeTag;
+
+  pub trait SerializeKind {
+    #[inline(always)]
+    fn blocking_kind(&self) -> SerializeTag {
+      SerializeTag
+    }
+
+    #[inline(always)]
+    fn async_kind(&self) -> SerializeTag {
+      SerializeTag
+    }
+  }
+
+  impl<T: Serialize> SerializeKind for &T {}
+
+  impl SerializeTag {
+    #[inline(always)]
+    pub fn block<P, T>(self, value: T, resolver: InvokeResolver<P>)
+    where
+      P: Params,
+      T: Serialize,
+    {
+      resolver.respond(Ok(value))
+    }
+
+    #[inline(always)]
+    pub fn future<T>(self, value: T) -> impl Future<Output = Result<Value, InvokeError>>
+    where
+      T: Serialize,
+    {
+      std::future::ready(serde_json::to_value(value).map_err(InvokeError::from_serde_json))
+    }
+  }
+
+  // ===== Result<impl Serialize, impl Into<InvokeError>> =====
+
+  pub struct ResultTag;
+
+  pub trait ResultKind {
+    #[inline(always)]
+    fn blocking_kind(&self) -> ResultTag {
+      ResultTag
+    }
+
+    #[inline(always)]
+    fn async_kind(&self) -> ResultTag {
+      ResultTag
+    }
+  }
+
+  impl<T: Serialize, E: Into<InvokeError>> ResultKind for Result<T, E> {}
+
+  impl ResultTag {
+    #[inline(always)]
+    pub fn block<P, T, E>(self, value: Result<T, E>, resolver: InvokeResolver<P>)
+    where
+      P: Params,
+      T: Serialize,
+      E: Into<InvokeError>,
+    {
+      resolver.respond(value.map_err(Into::into))
+    }
+
+    #[inline(always)]
+    pub fn future<T, E>(
+      self,
+      value: Result<T, E>,
+    ) -> impl Future<Output = Result<Value, InvokeError>>
+    where
+      T: Serialize,
+      E: Into<InvokeError>,
+    {
+      std::future::ready(
+        value
+          .map_err(Into::into)
+          .and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json)),
+      )
+    }
+  }
+
+  // ===== Future<Output = impl Serialize> =====
+
+  pub struct FutureTag;
+
+  pub trait FutureKind {
+    #[inline(always)]
+    fn async_kind(&self) -> FutureTag {
+      FutureTag
+    }
+  }
+  impl<T: Serialize, F: Future<Output = T>> FutureKind for &F {}
+
+  impl FutureTag {
+    #[inline(always)]
+    pub fn future<T, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
+    where
+      T: Serialize,
+      F: Future<Output = T> + Send + 'static,
+    {
+      value.map(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
+    }
+  }
+
+  // ===== Future<Output = Result<impl Serialize, impl Into<InvokeError>>> =====
+
+  pub struct ResultFutureTag;
+
+  pub trait ResultFutureKind {
+    #[inline(always)]
+    fn async_kind(&self) -> ResultFutureTag {
+      ResultFutureTag
+    }
+  }
+
+  impl<T: Serialize, E: Into<InvokeError>, F: Future<Output = Result<T, E>>> ResultFutureKind for F {}
+
+  impl ResultFutureTag {
+    #[inline(always)]
+    pub fn future<T, E, F>(self, value: F) -> impl Future<Output = Result<Value, InvokeError>>
+    where
+      T: Serialize,
+      E: Into<InvokeError>,
+      F: Future<Output = Result<T, E>> + Send,
+    {
+      value.err_into().map(|result| {
+        result.and_then(|value| serde_json::to_value(value).map_err(InvokeError::from_serde_json))
+      })
+    }
+  }
+}

+ 41 - 10
core/tauri/src/hooks.rs

@@ -49,12 +49,14 @@ pub struct InvokeError(JsonValue);
 
 impl InvokeError {
   /// Create an [`InvokeError`] as a string of the [`serde_json::Error`] message.
+  #[inline(always)]
   pub fn from_serde_json(error: serde_json::Error) -> Self {
     Self(JsonValue::String(error.to_string()))
   }
 }
 
 impl<T: Serialize> From<T> for InvokeError {
+  #[inline]
   fn from(value: T) -> Self {
     serde_json::to_value(value)
       .map(Self)
@@ -63,6 +65,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()))
   }
@@ -79,6 +82,7 @@ pub enum InvokeResponse {
 
 impl InvokeResponse {
   /// Turn a [`InvokeResponse`] back into a serializable result.
+  #[inline(always)]
   pub fn into_result(self) -> Result<JsonValue, JsonValue> {
     match self {
       Self::Ok(v) => Ok(v),
@@ -88,6 +92,7 @@ impl InvokeResponse {
 }
 
 impl<T: Serialize> From<Result<T, InvokeError>> for InvokeResponse {
+  #[inline]
   fn from(result: Result<T, InvokeError>) -> Self {
     match result {
       Ok(ok) => match serde_json::to_value(ok) {
@@ -99,9 +104,15 @@ impl<T: Serialize> From<Result<T, InvokeError>> for InvokeResponse {
   }
 }
 
+impl From<InvokeError> for InvokeResponse {
+  fn from(error: InvokeError) -> Self {
+    Self::Err(error)
+  }
+}
+
 /// Resolver of a invoke message.
-pub struct InvokeResolver<M: Params> {
-  window: Window<M>,
+pub struct InvokeResolver<P: Params> {
+  window: Window<P>,
   pub(crate) callback: String,
   pub(crate) error: String,
 }
@@ -126,6 +137,21 @@ impl<P: Params> InvokeResolver<P> {
     });
   }
 
+  /// 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<JsonValue, InvokeError>> + Send + 'static,
+  {
+    crate::async_runtime::spawn(async move {
+      Self::return_result(self.window, task.await.into(), self.callback, self.error);
+    });
+  }
+
+  /// Reply to the invoke promise with a serializable value.
+  pub fn respond<T: Serialize>(self, value: Result<T, InvokeError>) {
+    Self::return_result(self.window, value.into(), self.callback, self.error)
+  }
+
   /// Reply to the invoke promise running the given closure.
   pub fn respond_closure<T, F>(self, f: F)
   where
@@ -136,20 +162,25 @@ impl<P: Params> InvokeResolver<P> {
   }
 
   /// Resolve the invoke promise with a value.
-  pub fn resolve<S: Serialize>(self, value: S) {
-    Self::return_result(self.window, Ok(value), self.callback, self.error)
+  pub fn resolve<T: Serialize>(self, value: T) {
+    Self::return_result(self.window, Ok(value).into(), self.callback, self.error)
   }
 
   /// Reject the invoke promise with a value.
-  pub fn reject<S: Serialize>(self, value: S) {
+  pub fn reject<T: Serialize>(self, value: T) {
     Self::return_result(
       self.window,
-      Result::<(), _>::Err(value.into()),
+      Result::<(), _>::Err(value.into()).into(),
       self.callback,
       self.error,
     )
   }
 
+  /// Reject the invoke promise with an [`InvokeError`].
+  pub fn invoke_error(self, error: InvokeError) {
+    Self::return_result(self.window, error.into(), self.callback, self.error)
+  }
+
   /// Asynchronously executes the given task
   /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names.
   ///
@@ -174,17 +205,17 @@ impl<P: Params> InvokeResolver<P> {
     success_callback: String,
     error_callback: String,
   ) {
-    Self::return_result(window, f(), success_callback, error_callback)
+    Self::return_result(window, f().into(), success_callback, error_callback)
   }
 
-  pub(crate) fn return_result<T: Serialize>(
+  pub(crate) fn return_result(
     window: Window<P>,
-    response: Result<T, InvokeError>,
+    response: InvokeResponse,
     success_callback: String,
     error_callback: String,
   ) {
     let callback_string = match format_callback_result(
-      InvokeResponse::from(response).into_result(),
+      response.into_result(),
       success_callback,
       error_callback.clone(),
     ) {

+ 26 - 24
examples/commands/public/index.html

@@ -10,46 +10,48 @@
 
 <body>
   <h1>Tauri Commands</h1>
-  <div id="response">Response:</div>
+  <div>Response: <span id="response"></span></div>
+  <div >Without Args: <span id="response-optional"></span></div>
   <div id="container"></div>
   <script>
-    const responseDiv = document.querySelector('#response')
-    function runCommand(commandName, args) {
+    function runCommand(commandName, args, optional) {
+      const id = optional ? "#response-optional" : "#response";
+      const result = document.querySelector(id)
       window.__TAURI__.invoke(commandName, args).then(response => {
-        responseDiv.innerHTML = `Response: Ok(${response})`
+        result.innerText = `Ok(${response})`
       }).catch(error => {
-        responseDiv.innerHTML = `Response: Err(${error})`
+        result.innerText = `Err(${error})`
       })
     }
 
     const container = document.querySelector('#container')
     const commands = [
-      { name: 'window_label', required: true },
-      { name: 'simple_command', required: true },
-      { name: 'stateful_command', required: false },
-      { name: 'async_simple_command', required: true },
-      { name: 'async_stateful_command', required: false },
-      { name: 'simple_command_with_result', required: true },
-      { name: 'stateful_command_with_result', required: false },
-      { name: 'async_simple_command_with_result', required: true },
-      { name: 'async_stateful_command_with_result', required: false },
-      { name: 'command_arguments_wild', required: true },
-      { name: 'command_arguments_struct', required: true, args: { "Person": { "name": "ferris", age: 6 } } },
-      { name: 'command_arguments_tuple_struct', required: true, args: { "InlinePerson": [ "ferris", 6 ] } },
+      { name: 'borrow_cmd' },
+      { name: 'window_label' },
+      { name: 'simple_command' },
+      { name: 'stateful_command' },
+      { name: 'async_simple_command' },
+      { name: 'future_simple_command'},
+      { name: 'async_stateful_command' },
+      { name: 'simple_command_with_result' },
+      { name: 'stateful_command_with_result' },
+      { name: 'async_simple_command_with_result' },
+      { name: 'future_simple_command_with_return' },
+      { name: 'future_simple_command_with_result' },
+      { name: 'async_stateful_command_with_result' },
+      { name: 'command_arguments_wild' },
+      { name: 'command_arguments_struct', args: { "Person": { "name": "ferris", age: 6 } } },
+      { name: 'command_arguments_tuple_struct', args: { "InlinePerson": [ "ferris", 6 ] } },
     ]
 
     for (const command of commands) {
-      const { name, required } = command
+      const { name } = command
       const args = command.args ?? { argument: 'value' }
       const button = document.createElement('button')
       button.innerHTML = `Run ${name}`;
       button.addEventListener("click", function () {
-        runCommand(name, args)
-        if (!required) {
-          setTimeout(() => {
-            runCommand(name, {})
-          }, 1000)
-        }
+        runCommand(name, args, false)
+        runCommand(name, Object.create(null), true)
       });
       container.appendChild(button);
     }

+ 5 - 3
examples/commands/src-tauri/src/commands.rs

@@ -2,12 +2,14 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-#[tauri::command]
+use tauri::{command, State};
+
+#[command]
 pub fn simple_command(argument: String) {
   println!("{}", argument);
 }
 
-#[tauri::command]
-pub fn stateful_command(argument: Option<String>, state: tauri::State<'_, super::MyState>) {
+#[command]
+pub fn stateful_command(argument: Option<String>, state: State<'_, super::MyState>) {
   println!("{:?} {:?}", argument, state.inner());
 }

+ 70 - 9
examples/commands/src-tauri/src/main.rs

@@ -19,6 +19,11 @@ pub struct MyState {
   label: String,
 }
 
+#[derive(Debug, serde::Serialize)]
+enum MyError {
+  FooError,
+}
+
 // ------------------------ Commands using Window ------------------------
 #[command]
 fn window_label(window: Window<impl Params<Label = String>>) {
@@ -33,33 +38,72 @@ async fn async_simple_command(argument: String) {
 }
 
 #[command]
-async fn async_stateful_command(argument: Option<String>, state: State<'_, MyState>) {
+async fn async_stateful_command(
+  argument: Option<String>,
+  state: State<'_, MyState>,
+) -> Result<(), ()> {
   println!("{:?} {:?}", argument, state.inner());
+  Ok(())
 }
 
-// ------------------------ Commands returning Result ------------------------
+// Raw future commands
+#[command(async)]
+fn future_simple_command(argument: String) -> impl std::future::Future<Output = ()> {
+  println!("{}", argument);
+  std::future::ready(())
+}
+
+#[command(async)]
+fn future_simple_command_with_return(
+  argument: String,
+) -> impl std::future::Future<Output = String> {
+  println!("{}", argument);
+  std::future::ready(argument)
+}
+
+#[command(async)]
+fn future_simple_command_with_result(
+  argument: String,
+) -> impl std::future::Future<Output = Result<String, ()>> {
+  println!("{}", argument);
+  std::future::ready(Ok(argument))
+}
+
+#[command(async)]
+fn force_async(argument: String) -> String {
+  argument
+}
 
-type Result<T> = std::result::Result<T, ()>;
+#[command(async)]
+fn force_async_with_result(argument: &str) -> Result<&str, MyError> {
+  (!argument.is_empty())
+    .then(|| argument)
+    .ok_or(MyError::FooError)
+}
+
+// ------------------------ Commands returning Result ------------------------
 
 #[command]
-fn simple_command_with_result(argument: String) -> Result<String> {
+fn simple_command_with_result(argument: String) -> Result<String, MyError> {
   println!("{}", argument);
-  (!argument.is_empty()).then(|| argument).ok_or(())
+  (!argument.is_empty())
+    .then(|| argument)
+    .ok_or(MyError::FooError)
 }
 
 #[command]
 fn stateful_command_with_result(
   argument: Option<String>,
   state: State<'_, MyState>,
-) -> Result<String> {
+) -> Result<String, MyError> {
   println!("{:?} {:?}", argument, state.inner());
-  argument.ok_or(())
+  dbg!(argument.ok_or(MyError::FooError))
 }
 
 // Async commands
 
 #[command]
-async fn async_simple_command_with_result(argument: String) -> Result<String> {
+async fn async_simple_command_with_result(argument: String) -> Result<String, MyError> {
   println!("{}", argument);
   Ok(argument)
 }
@@ -68,7 +112,7 @@ async fn async_simple_command_with_result(argument: String) -> Result<String> {
 async fn async_stateful_command_with_result(
   argument: Option<String>,
   state: State<'_, MyState>,
-) -> Result<String> {
+) -> Result<String, MyError> {
   println!("{:?} {:?}", argument, state.inner());
   Ok(argument.unwrap_or_else(|| "".to_string()))
 }
@@ -99,6 +143,16 @@ fn command_arguments_tuple_struct(InlinePerson(name, age): InlinePerson) {
   println!("received person tuple with name: {} | age: {}", name, age)
 }
 
+#[command]
+fn borrow_cmd(argument: &str) -> &str {
+  argument
+}
+
+#[command]
+fn borrow_cmd_async(argument: &str) -> &str {
+  argument
+}
+
 fn main() {
   tauri::Builder::default()
     .manage(MyState {
@@ -106,10 +160,15 @@ fn main() {
       label: "Tauri!".into(),
     })
     .invoke_handler(tauri::generate_handler![
+      borrow_cmd,
+      borrow_cmd_async,
       window_label,
+      force_async,
+      force_async_with_result,
       commands::simple_command,
       commands::stateful_command,
       async_simple_command,
+      future_simple_command,
       async_stateful_command,
       command_arguments_wild,
       command_arguments_struct,
@@ -117,6 +176,8 @@ fn main() {
       stateful_command_with_result,
       command_arguments_tuple_struct,
       async_simple_command_with_result,
+      future_simple_command_with_return,
+      future_simple_command_with_result,
       async_stateful_command_with_result,
     ])
     .run(tauri::generate_context!())