Pārlūkot izejas kodu

feat(core): add state management, closes #1655 (#1665)

* feat(core): add state management, closes #1655

* fix(tests): ignore doc example

* use a trait to manage #[command] parameters

* add docs [skip ci]

* finish command before moving into respond_async

* Revert "finish command before moving into respond_async"

This reverts commit 4651bed5bf2bb91dbab9fce4807578ed67cf0468.

* refactor: split InvokeMessage into InvokeResolver, add InvokeResponse

* feat: add managed state to the plugin interface

* feat: add commands example

* add change file [skip ci]

* cleanup clones

Co-authored-by: chip reed <chip@chip.sh>
Lucas Fernandes Nogueira 4 gadi atpakaļ
vecāks
revīzija
8b6f3de0ad
43 mainītis faili ar 1070 papildinājumiem un 222 dzēšanām
  1. 5 0
      .changes/app-state.md
  2. 5 0
      .changes/command-state.md
  3. 7 0
      .changes/plugin-refactor.md
  4. 5 0
      .changes/remove-with-window.md
  5. 1 0
      Cargo.toml
  6. 72 67
      core/tauri-macros/src/command.rs
  7. 3 4
      core/tauri-macros/src/lib.rs
  8. 1 0
      core/tauri/Cargo.toml
  9. 300 0
      core/tauri/src/command.rs
  10. 53 22
      core/tauri/src/endpoints.rs
  11. 119 71
      core/tauri/src/hooks.rs
  12. 39 15
      core/tauri/src/lib.rs
  13. 12 9
      core/tauri/src/plugin.rs
  14. 65 6
      core/tauri/src/runtime/app.rs
  15. 29 15
      core/tauri/src/runtime/manager.rs
  16. 13 0
      core/tauri/src/runtime/webview.rs
  17. 31 7
      core/tauri/src/runtime/window.rs
  18. 63 0
      core/tauri/src/state.rs
  19. 2 6
      examples/api/src-tauri/src/cmd.rs
  20. 7 0
      examples/commands/package.json
  21. 55 0
      examples/commands/public/index.html
  22. 10 0
      examples/commands/src-tauri/.gitignore
  23. 1 0
      examples/commands/src-tauri/.license_template
  24. 17 0
      examples/commands/src-tauri/Cargo.toml
  25. 7 0
      examples/commands/src-tauri/build.rs
  26. BIN
      examples/commands/src-tauri/icons/128x128.png
  27. BIN
      examples/commands/src-tauri/icons/128x128@2x.png
  28. BIN
      examples/commands/src-tauri/icons/32x32.png
  29. BIN
      examples/commands/src-tauri/icons/Square107x107Logo.png
  30. BIN
      examples/commands/src-tauri/icons/Square142x142Logo.png
  31. BIN
      examples/commands/src-tauri/icons/Square150x150Logo.png
  32. BIN
      examples/commands/src-tauri/icons/Square284x284Logo.png
  33. BIN
      examples/commands/src-tauri/icons/Square30x30Logo.png
  34. BIN
      examples/commands/src-tauri/icons/Square310x310Logo.png
  35. BIN
      examples/commands/src-tauri/icons/Square44x44Logo.png
  36. BIN
      examples/commands/src-tauri/icons/Square71x71Logo.png
  37. BIN
      examples/commands/src-tauri/icons/Square89x89Logo.png
  38. BIN
      examples/commands/src-tauri/icons/StoreLogo.png
  39. BIN
      examples/commands/src-tauri/icons/icon.icns
  40. BIN
      examples/commands/src-tauri/icons/icon.ico
  41. BIN
      examples/commands/src-tauri/icons/icon.png
  42. 92 0
      examples/commands/src-tauri/src/main.rs
  43. 56 0
      examples/commands/src-tauri/tauri.conf.json

+ 5 - 0
.changes/app-state.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Adds `manage` API to the `Builder` struct, which manages app state.

+ 5 - 0
.changes/command-state.md

@@ -0,0 +1,5 @@
+---
+"tauri-macros": patch
+---
+
+Adds support to command state, triggered when a command argument is `arg: State<'_, StateType>`.

+ 7 - 0
.changes/plugin-refactor.md

@@ -0,0 +1,7 @@
+---
+"tauri": patch
+---
+
+Refactored the `Plugin` trait `initialize` and `extend_api` signatures.
+`initialize` now takes the `App` as first argument, and `extend_api` takes a `InvokeResolver` as last argument.
+This adds support to managed state on plugins.

+ 5 - 0
.changes/remove-with-window.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Removes the `with_window` attribute on the `command` macro. Tauri now infers it using the `FromCommand` trait.

+ 1 - 0
Cargo.toml

@@ -11,6 +11,7 @@ members = [
   "examples/api/src-tauri",
   "examples/helloworld/src-tauri",
   "examples/multiwindow/src-tauri",
+  "examples/commands/src-tauri",
   # used to build updater artifacts
   "examples/updater/src-tauri",
 ]

+ 72 - 67
core/tauri-macros/src/command.rs

@@ -3,28 +3,35 @@
 // SPDX-License-Identifier: MIT
 
 use proc_macro2::TokenStream;
-use quote::{format_ident, quote};
+use quote::{format_ident, quote, TokenStreamExt};
 use syn::{
-  parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Meta, NestedMeta, Pat, Path,
-  ReturnType, Token, Type,
+  parse::Parser, punctuated::Punctuated, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Token, Type,
+  Visibility,
 };
 
-pub fn generate_command(attrs: Vec<NestedMeta>, function: ItemFn) -> TokenStream {
-  // Check if "with_window" attr was passed to macro
-  let with_window = attrs.iter().any(|a| {
-    if let NestedMeta::Meta(Meta::Path(path)) = a {
-      path
-        .get_ident()
-        .map(|i| *i == "with_window")
-        .unwrap_or(false)
-    } else {
-      false
+fn fn_wrapper(function: &ItemFn) -> (&Visibility, Ident) {
+  (
+    &function.vis,
+    format_ident!("{}_wrapper", function.sig.ident),
+  )
+}
+
+fn err(function: ItemFn, error_message: &str) -> TokenStream {
+  let (vis, wrap) = fn_wrapper(&function);
+  quote! {
+    #function
+
+    #vis fn #wrap<P: ::tauri::Params>(_message: ::tauri::InvokeMessage<P>) {
+      compile_error!(#error_message);
+      unimplemented!()
     }
-  });
+  }
+}
 
+pub fn generate_command(function: ItemFn) -> TokenStream {
   let fn_name = function.sig.ident.clone();
   let fn_name_str = fn_name.to_string();
-  let fn_wrapper = format_ident!("{}_wrapper", fn_name);
+  let (vis, fn_wrapper) = fn_wrapper(&function);
   let returns_result = match function.sig.output {
     ReturnType::Type(_, ref ty) => match &**ty {
       Type::Path(type_path) => {
@@ -40,40 +47,45 @@ pub fn generate_command(attrs: Vec<NestedMeta>, function: ItemFn) -> TokenStream
     ReturnType::Default => false,
   };
 
-  // Split function args into names and types
-  let (mut names, mut types): (Vec<Ident>, Vec<Path>) = function
-    .sig
-    .inputs
-    .iter()
-    .map(|param| {
-      let mut arg_name = None;
-      let mut arg_type = None;
-      if let FnArg::Typed(arg) = param {
-        if let Pat::Ident(ident) = arg.pat.as_ref() {
-          arg_name = Some(ident.ident.clone());
-        }
-        if let Type::Path(path) = arg.ty.as_ref() {
-          arg_type = Some(path.path.clone());
-        }
+  let mut invoke_arg_names: Vec<Ident> = Default::default();
+  let mut invoke_arg_types: Vec<Path> = Default::default();
+  let mut invoke_args: TokenStream = Default::default();
+
+  for param in &function.sig.inputs {
+    let mut arg_name = None;
+    let mut arg_type = None;
+    if let FnArg::Typed(arg) = param {
+      if let Pat::Ident(ident) = arg.pat.as_ref() {
+        arg_name = Some(ident.ident.clone());
+      }
+      if let Type::Path(path) = arg.ty.as_ref() {
+        arg_type = Some(path.path.clone());
       }
-      (
-        arg_name.clone().unwrap(),
-        arg_type.unwrap_or_else(|| panic!("Invalid type for arg \"{}\"", arg_name.unwrap())),
-      )
-    })
-    .unzip();
-
-  let window_arg_maybe = match types.first() {
-    Some(_) if with_window => {
-      // Remove window arg from list so it isn't expected as arg from JS
-      types.drain(0..1);
-      names.drain(0..1);
-      // Tell wrapper to pass `window` to original function
-      quote!(_window,)
     }
-    // Tell wrapper not to pass `window` to original function
-    _ => quote!(),
-  };
+
+    let arg_name_ = arg_name.clone().unwrap();
+    let arg_name_s = arg_name_.to_string();
+
+    let arg_type = match arg_type {
+      Some(arg_type) => arg_type,
+      None => {
+        return err(
+          function.clone(),
+          &format!("invalid type for arg: {}", arg_name_),
+        )
+      }
+    };
+
+    invoke_args.append_all(quote! {
+      let #arg_name_ = match <#arg_type>::from_command(#fn_name_str, #arg_name_s, &message) {
+        Ok(value) => value,
+        Err(e) => return tauri::InvokeResponse::error(::tauri::Error::InvalidArgs(#fn_name_str, e).to_string())
+      };
+    });
+    invoke_arg_names.push(arg_name_.clone());
+    invoke_arg_types.push(arg_type);
+  }
+
   let await_maybe = if function.sig.asyncness.is_some() {
     quote!(.await)
   } else {
@@ -86,30 +98,23 @@ pub fn generate_command(attrs: Vec<NestedMeta>, function: ItemFn) -> TokenStream
   // note that all types must implement `serde::Serialize`.
   let return_value = if returns_result {
     quote! {
-      match #fn_name(#window_arg_maybe #(parsed_args.#names),*)#await_maybe {
-        Ok(value) => ::core::result::Result::Ok(value),
-        Err(e) => ::core::result::Result::Err(e),
+      match #fn_name(#(#invoke_arg_names),*)#await_maybe {
+        Ok(value) => ::core::result::Result::<_, ()>::Ok(value).into(),
+        Err(e) => ::core::result::Result::<(), _>::Err(e).into(),
       }
     }
   } else {
-    quote! { ::core::result::Result::<_, ()>::Ok(#fn_name(#window_arg_maybe #(parsed_args.#names),*)#await_maybe) }
+    quote! { ::core::result::Result::<_, ()>::Ok(#fn_name(#(#invoke_arg_names),*)#await_maybe).into() }
   };
 
   quote! {
     #function
-    pub fn #fn_wrapper<P: ::tauri::Params>(message: ::tauri::InvokeMessage<P>) {
-      #[derive(::serde::Deserialize)]
-      #[serde(rename_all = "camelCase")]
-      struct ParsedArgs {
-        #(#names: #types),*
-      }
-      let _window = message.window();
-      match ::serde_json::from_value::<ParsedArgs>(message.payload()) {
-        Ok(parsed_args) => message.respond_async(async move {
-          #return_value
-        }),
-        Err(e) => message.reject(::tauri::Error::InvalidArgs(#fn_name_str, e).to_string()),
-      }
+    #vis fn #fn_wrapper<P: ::tauri::Params>(message: ::tauri::InvokeMessage<P>, resolver: ::tauri::InvokeResolver<P>) {
+      use ::tauri::command::FromCommand;
+      resolver.respond_async(async move {
+        #invoke_args
+        #return_value
+      })
     }
   }
 }
@@ -134,12 +139,12 @@ pub fn generate_handler(item: proc_macro::TokenStream) -> TokenStream {
   });
 
   quote! {
-    move |message| {
+    move |message, resolver| {
       let cmd = message.command().to_string();
       match cmd.as_str() {
-        #(stringify!(#fn_names) => #fn_wrappers(message),)*
+        #(stringify!(#fn_names) => #fn_wrappers(message, resolver),)*
         _ => {
-          message.reject(format!("command {} not found", cmd))
+          resolver.reject(format!("command {} not found", cmd))
         },
       }
     }

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

@@ -5,7 +5,7 @@
 extern crate proc_macro;
 use crate::context::ContextItems;
 use proc_macro::TokenStream;
-use syn::{parse_macro_input, AttributeArgs, ItemFn};
+use syn::{parse_macro_input, ItemFn};
 
 mod command;
 
@@ -13,10 +13,9 @@ mod command;
 mod context;
 
 #[proc_macro_attribute]
-pub fn command(attrs: TokenStream, item: TokenStream) -> TokenStream {
+pub fn command(_attrs: TokenStream, item: TokenStream) -> TokenStream {
   let function = parse_macro_input!(item as ItemFn);
-  let attrs = parse_macro_input!(attrs as AttributeArgs);
-  let gen = command::generate_command(attrs, function);
+  let gen = command::generate_command(function);
   gen.into()
 }
 

+ 1 - 0
core/tauri/Cargo.toml

@@ -48,6 +48,7 @@ shared_child = "0.3"
 os_pipe = "0.9"
 minisign-verify = "0.1.8"
 image = "0.23"
+state = "0.4"
 
 [build-dependencies]
 cfg_aliases = "0.1.1"

+ 300 - 0
core/tauri/src/command.rs

@@ -0,0 +1,300 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+//! Useful items for custom commands.
+
+use crate::{InvokeMessage, Params};
+use serde::de::Visitor;
+use serde::Deserializer;
+
+/// A [`Deserializer`] wrapper around [`Value::get`].
+///
+/// If the key doesn't exist, an error will be returned if the deserialized type is not expecting
+/// an optional item. If the key does exist, the value will be called with [`Value`]'s
+/// [`Deserializer`] implementation.
+struct KeyedValue<'de> {
+  command: &'de str,
+  key: &'de str,
+  value: &'de serde_json::Value,
+}
+
+macro_rules! kv_value {
+  ($s:ident) => {{
+    use serde::de::Error;
+
+    match $s.value.get($s.key) {
+      Some(value) => value,
+      None => {
+        return Err(serde_json::Error::custom(format!(
+          "command {} missing required key `{}`",
+          $s.command, $s.key
+        )))
+      }
+    }
+  }};
+}
+
+impl<'de> Deserializer<'de> for KeyedValue<'de> {
+  type Error = serde_json::Error;
+
+  fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_any(visitor)
+  }
+
+  fn deserialize_bool<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_bool(visitor)
+  }
+
+  fn deserialize_i8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_i8(visitor)
+  }
+
+  fn deserialize_i16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_i16(visitor)
+  }
+
+  fn deserialize_i32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_i32(visitor)
+  }
+
+  fn deserialize_i64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_i64(visitor)
+  }
+
+  fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_u8(visitor)
+  }
+
+  fn deserialize_u16<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_u16(visitor)
+  }
+
+  fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_u32(visitor)
+  }
+
+  fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_u64(visitor)
+  }
+
+  fn deserialize_f32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_f32(visitor)
+  }
+
+  fn deserialize_f64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_f64(visitor)
+  }
+
+  fn deserialize_char<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_char(visitor)
+  }
+
+  fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_str(visitor)
+  }
+
+  fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_string(visitor)
+  }
+
+  fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_bytes(visitor)
+  }
+
+  fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_byte_buf(visitor)
+  }
+
+  fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    match self.value.get(self.key) {
+      Some(value) => value.deserialize_option(visitor),
+      None => visitor.visit_none(),
+    }
+  }
+
+  fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_unit(visitor)
+  }
+
+  fn deserialize_unit_struct<V>(
+    self,
+    name: &'static str,
+    visitor: V,
+  ) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_unit_struct(name, visitor)
+  }
+
+  fn deserialize_newtype_struct<V>(
+    self,
+    name: &'static str,
+    visitor: V,
+  ) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_newtype_struct(name, visitor)
+  }
+
+  fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_seq(visitor)
+  }
+
+  fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_tuple(len, visitor)
+  }
+
+  fn deserialize_tuple_struct<V>(
+    self,
+    name: &'static str,
+    len: usize,
+    visitor: V,
+  ) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_tuple_struct(name, len, visitor)
+  }
+
+  fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_map(visitor)
+  }
+
+  fn deserialize_struct<V>(
+    self,
+    name: &'static str,
+    fields: &'static [&'static str],
+    visitor: V,
+  ) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_struct(name, fields, visitor)
+  }
+
+  fn deserialize_enum<V>(
+    self,
+    name: &'static str,
+    variants: &'static [&'static str],
+    visitor: V,
+  ) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_enum(name, variants, visitor)
+  }
+
+  fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_identifier(visitor)
+  }
+
+  fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
+  where
+    V: Visitor<'de>,
+  {
+    kv_value!(self).deserialize_ignored_any(visitor)
+  }
+}
+
+/// Trait implemented by command arguments to derive a value from a [`InvokeMessage`].
+/// [`tauri::Window`], [`tauri::State`] and types that implements [`Deserialize`] automatically implements this trait.
+pub trait FromCommand<'de, P: Params>: Sized {
+  /// Derives an instance of `Self` from the [`InvokeMessage`].
+  /// If the derivation fails, the corresponding message will be rejected using [`InvokeMessage#reject`].
+  ///
+  /// # Arguments
+  /// - `command`: the command value passed to invoke, e.g. `initialize` on `invoke('initialize', {})`.
+  /// - `key`: The name of the variable in the command handler, e.g. `value` on `#[command] fn handler(value: u64)`
+  /// - `message`: The [`InvokeMessage`] instance.
+  fn from_command(
+    command: &'de str,
+    key: &'de str,
+    message: &'de InvokeMessage<P>,
+  ) -> ::core::result::Result<Self, serde_json::Error>;
+}
+
+impl<'de, D: serde::Deserialize<'de>, P: Params> FromCommand<'de, P> for D {
+  fn from_command(
+    command: &'de str,
+    key: &'de str,
+    message: &'de InvokeMessage<P>,
+  ) -> ::core::result::Result<Self, serde_json::Error> {
+    D::deserialize(KeyedValue {
+      command,
+      key,
+      value: &message.payload,
+    })
+  }
+}

+ 53 - 22
core/tauri/src/endpoints.rs

@@ -4,8 +4,8 @@
 
 use crate::{
   api::{config::Config, PackageInfo},
-  hooks::InvokeMessage,
-  Params,
+  hooks::{InvokeMessage, InvokeResolver},
+  Params, Window,
 };
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
@@ -54,77 +54,106 @@ enum Module {
 }
 
 impl Module {
-  fn run<M: Params>(self, message: InvokeMessage<M>, config: &Config, package_info: PackageInfo) {
-    let window = message.window();
+  fn run<M: Params>(
+    self,
+    window: Window<M>,
+    resolver: InvokeResolver<M>,
+    config: &Config,
+    package_info: PackageInfo,
+  ) {
     match self {
-      Self::App(cmd) => message.respond_async(async move {
+      Self::App(cmd) => resolver.respond_async(async move {
         cmd
           .run(package_info)
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::Process(cmd) => message
-        .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }),
-      Self::Fs(cmd) => message
-        .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }),
-      Self::Window(cmd) => message.respond_async(async move {
+      Self::Process(cmd) => resolver.respond_async(async move {
+        cmd
+          .run()
+          .and_then(|r| r.json)
+          .map_err(|e| e.to_string())
+          .into()
+      }),
+      Self::Fs(cmd) => resolver.respond_async(async move {
+        cmd
+          .run()
+          .and_then(|r| r.json)
+          .map_err(|e| e.to_string())
+          .into()
+      }),
+      Self::Window(cmd) => resolver.respond_async(async move {
         cmd
           .run(window)
           .await
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::Shell(cmd) => message.respond_async(async move {
+      Self::Shell(cmd) => resolver.respond_async(async move {
         cmd
           .run(window)
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::Event(cmd) => message.respond_async(async move {
+      Self::Event(cmd) => resolver.respond_async(async move {
         cmd
           .run(window)
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::Internal(cmd) => message.respond_async(async move {
+      Self::Internal(cmd) => resolver.respond_async(async move {
         cmd
           .run(window)
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
+      }),
+      Self::Dialog(cmd) => resolver.respond_async(async move {
+        cmd
+          .run()
+          .and_then(|r| r.json)
+          .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::Dialog(cmd) => message
-        .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }),
       Self::Cli(cmd) => {
         if let Some(cli_config) = config.tauri.cli.clone() {
-          message.respond_async(async move {
+          resolver.respond_async(async move {
             cmd
               .run(&cli_config)
               .and_then(|r| r.json)
               .map_err(|e| e.to_string())
+              .into()
           })
         }
       }
       Self::Notification(cmd) => {
         let identifier = config.tauri.bundle.identifier.clone();
-        message.respond_async(async move {
+        resolver.respond_async(async move {
           cmd
             .run(identifier)
             .and_then(|r| r.json)
             .map_err(|e| e.to_string())
+            .into()
         })
       }
-      Self::Http(cmd) => message.respond_async(async move {
+      Self::Http(cmd) => resolver.respond_async(async move {
         cmd
           .run()
           .await
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
-      Self::GlobalShortcut(cmd) => message.respond_async(async move {
+      Self::GlobalShortcut(cmd) => resolver.respond_async(async move {
         cmd
           .run(window)
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
+          .into()
       }),
     }
   }
@@ -133,15 +162,17 @@ impl Module {
 pub(crate) fn handle<M: Params>(
   module: String,
   message: InvokeMessage<M>,
+  resolver: InvokeResolver<M>,
   config: &Config,
   package_info: &PackageInfo,
 ) {
-  let mut payload = message.payload();
+  let mut payload = message.payload;
   if let JsonValue::Object(ref mut obj) = payload {
     obj.insert("module".to_string(), JsonValue::String(module));
   }
+  let window = message.window;
   match serde_json::from_value::<Module>(payload) {
-    Ok(module) => module.run(message, config, package_info.clone()),
-    Err(e) => message.reject(e.to_string()),
+    Ok(module) => module.run(window, resolver, config, package_info.clone()),
+    Err(e) => resolver.reject(e.to_string()),
   }
 }

+ 119 - 71
core/tauri/src/hooks.rs

@@ -5,16 +5,17 @@
 use crate::{
   api::rpc::{format_callback, format_callback_result},
   runtime::app::App,
-  Params, Window,
+  Params, StateManager, Window,
 };
 use serde::{Deserialize, Serialize};
-use std::future::Future;
+use serde_json::Value as JsonValue;
+use std::{future::Future, sync::Arc};
 
 /// A closure that is run when the Tauri application is setting up.
 pub type SetupHook<M> = Box<dyn Fn(&mut App<M>) -> Result<(), Box<dyn std::error::Error>> + Send>;
 
 /// A closure that is run everytime Tauri receives a message it doesn't explicitly handle.
-pub type InvokeHandler<M> = dyn Fn(InvokeMessage<M>) + Send + Sync + 'static;
+pub type InvokeHandler<M> = dyn Fn(InvokeMessage<M>, InvokeResolver<M>) + Send + Sync + 'static;
 
 /// A closure that is run once every time a window is created and loaded.
 pub type OnPageLoad<M> = dyn Fn(Window<M>, PageLoadPayload) + Send + Sync + 'static;
@@ -32,85 +33,91 @@ impl PageLoadPayload {
   }
 }
 
-/// Payload from an invoke call.
-#[derive(Debug, Deserialize)]
-pub(crate) struct InvokePayload {
-  #[serde(rename = "__tauriModule")]
-  pub(crate) tauri_module: Option<String>,
-  pub(crate) callback: String,
-  pub(crate) error: String,
-  #[serde(rename = "mainThread", default)]
-  pub(crate) main_thread: bool,
-  #[serde(flatten)]
-  pub(crate) inner: serde_json::Value,
+/// Response from a [`InvokeMessage`] passed to the [`InvokeResolver`].
+#[derive(Debug)]
+pub enum InvokeResponse {
+  /// Resolve the promise.
+  Ok(JsonValue),
+  /// Reject the promise.
+  Err(JsonValue),
 }
 
-/// An invoke message.
-pub struct InvokeMessage<M: Params> {
-  window: Window<M>,
-  pub(crate) command: String,
-
-  /// Allow our crate to access the payload without cloning it.
-  pub(crate) payload: InvokePayload,
+impl<T: Serialize, E: Serialize> From<Result<T, E>> for InvokeResponse {
+  fn from(result: Result<T, E>) -> Self {
+    match result {
+      Result::Ok(t) => match serde_json::to_value(t) {
+        Ok(v) => Self::Ok(v),
+        Err(e) => Self::Err(JsonValue::String(e.to_string())),
+      },
+      Result::Err(e) => Self::error(e),
+    }
+  }
 }
 
-impl<M: Params> InvokeMessage<M> {
-  /// Create an new [`InvokeMessage`] from a payload send to a window.
-  pub(crate) fn new(window: Window<M>, command: String, payload: InvokePayload) -> Self {
-    Self {
-      window,
-      command,
-      payload,
+impl InvokeResponse {
+  #[doc(hidden)]
+  pub fn error<T: Serialize>(value: T) -> Self {
+    match serde_json::to_value(value) {
+      Ok(v) => Self::Err(v),
+      Err(e) => Self::Err(JsonValue::String(e.to_string())),
     }
   }
+}
 
-  /// The invoke command.
-  pub fn command(&self) -> &str {
-    &self.command
-  }
+/// Resolver of a invoke message.
+pub struct InvokeResolver<M: Params> {
+  window: Window<M>,
+  pub(crate) main_thread: bool,
+  pub(crate) callback: String,
+  pub(crate) error: String,
+}
 
-  /// The invoke payload.
-  pub fn payload(&self) -> serde_json::Value {
-    self.payload.inner.clone()
+/*impl<P: Params> Clone for InvokeResolver<P> {
+  fn clone(&self) -> Self {
+    Self {
+      window: self.window.clone(),
+      main_thread: self.main_thread,
+      callback: self.callback.clone(),
+      error: self.error.clone(),
+    }
   }
+}*/
 
-  /// The window that received the invoke.
-  pub fn window(&self) -> Window<M> {
-    self.window.clone()
+impl<M: Params> InvokeResolver<M> {
+  pub(crate) fn new(window: Window<M>, main_thread: bool, callback: String, error: String) -> Self {
+    Self {
+      window,
+      main_thread,
+      callback,
+      error,
+    }
   }
 
   /// Reply to the invoke promise with an async task.
-  pub fn respond_async<
-    T: Serialize,
-    Err: Serialize,
-    F: Future<Output = Result<T, Err>> + Send + 'static,
-  >(
-    self,
-    task: F,
-  ) {
-    if self.payload.main_thread {
+  pub fn respond_async<F: Future<Output = InvokeResponse> + Send + 'static>(self, task: F) {
+    if self.main_thread {
       crate::async_runtime::block_on(async move {
-        Self::return_task(self.window, task, self.payload.callback, self.payload.error).await;
+        Self::return_task(self.window, task, self.callback, self.error).await;
       });
     } else {
       crate::async_runtime::spawn(async move {
-        Self::return_task(self.window, task, self.payload.callback, self.payload.error).await;
+        Self::return_task(self.window, task, self.callback, self.error).await;
       });
     }
   }
 
   /// Reply to the invoke promise running the given closure.
-  pub fn respond_closure<T: Serialize, Err: Serialize, F: FnOnce() -> Result<T, Err>>(self, f: F) {
-    Self::return_closure(self.window, f, self.payload.callback, self.payload.error)
+  pub fn respond_closure<F: FnOnce() -> InvokeResponse>(self, f: F) {
+    Self::return_closure(self.window, f, self.callback, self.error)
   }
 
   /// Resolve the invoke promise with a value.
   pub fn resolve<S: Serialize>(self, value: S) {
     Self::return_result(
       self.window,
-      Result::<S, ()>::Ok(value),
-      self.payload.callback,
-      self.payload.error,
+      Result::<S, ()>::Ok(value).into(),
+      self.callback,
+      self.error,
     )
   }
 
@@ -118,9 +125,9 @@ impl<M: Params> InvokeMessage<M> {
   pub fn reject<S: Serialize>(self, value: S) {
     Self::return_result(
       self.window,
-      Result::<(), S>::Err(value),
-      self.payload.callback,
-      self.payload.error,
+      Result::<(), S>::Err(value).into(),
+      self.callback,
+      self.error,
     )
   }
 
@@ -129,11 +136,7 @@ impl<M: Params> InvokeMessage<M> {
   ///
   /// 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.
-  pub async fn return_task<
-    T: Serialize,
-    Err: Serialize,
-    F: std::future::Future<Output = Result<T, Err>> + Send + 'static,
-  >(
+  pub async fn return_task<F: std::future::Future<Output = InvokeResponse> + Send + 'static>(
     window: Window<M>,
     task: F,
     success_callback: String,
@@ -143,7 +146,7 @@ impl<M: Params> InvokeMessage<M> {
     Self::return_closure(window, || result, success_callback, error_callback)
   }
 
-  pub(crate) fn return_closure<T: Serialize, Err: Serialize, F: FnOnce() -> Result<T, Err>>(
+  pub(crate) fn return_closure<F: FnOnce() -> InvokeResponse>(
     window: Window<M>,
     f: F,
     success_callback: String,
@@ -152,19 +155,64 @@ impl<M: Params> InvokeMessage<M> {
     Self::return_result(window, f(), success_callback, error_callback)
   }
 
-  pub(crate) fn return_result<T: Serialize, Err: Serialize>(
+  pub(crate) fn return_result(
     window: Window<M>,
-    result: Result<T, Err>,
+    response: InvokeResponse,
     success_callback: String,
     error_callback: String,
   ) {
-    let callback_string =
-      match format_callback_result(result, success_callback, error_callback.clone()) {
-        Ok(callback_string) => callback_string,
-        Err(e) => format_callback(error_callback, &e.to_string())
-          .expect("unable to serialize shortcut string to json"),
-      };
+    let callback_string = match format_callback_result(
+      match response {
+        InvokeResponse::Ok(t) => std::result::Result::Ok(t),
+        InvokeResponse::Err(e) => std::result::Result::Err(e),
+      },
+      success_callback,
+      error_callback.clone(),
+    ) {
+      Ok(callback_string) => callback_string,
+      Err(e) => format_callback(error_callback, &e.to_string())
+        .expect("unable to serialize shortcut string to json"),
+    };
 
     let _ = window.eval(&callback_string);
   }
 }
+
+/// An invoke message.
+pub struct InvokeMessage<M: Params> {
+  /// The window that received the invoke message.
+  pub(crate) window: Window<M>,
+  /// Application managed state.
+  pub(crate) state: Arc<StateManager>,
+  /// The RPC command.
+  pub(crate) command: String,
+  /// The JSON argument passed on the invoke message.
+  pub(crate) payload: JsonValue,
+}
+
+impl<M: Params> InvokeMessage<M> {
+  /// Create an new [`InvokeMessage`] from a payload send to a window.
+  pub(crate) fn new(
+    window: Window<M>,
+    state: Arc<StateManager>,
+    command: String,
+    payload: JsonValue,
+  ) -> Self {
+    Self {
+      window,
+      state,
+      command,
+      payload,
+    }
+  }
+
+  /// The invoke command.
+  pub fn command(&self) -> &str {
+    &self.command
+  }
+
+  /// The window that received the invoke.
+  pub fn window(&self) -> Window<M> {
+    self.window.clone()
+  }
+}

+ 39 - 15
core/tauri/src/lib.rs

@@ -18,6 +18,7 @@ pub use tauri_macros::{command, generate_handler};
 pub mod api;
 /// Async runtime.
 pub mod async_runtime;
+pub mod command;
 /// The Tauri API endpoints.
 mod endpoints;
 mod error;
@@ -27,6 +28,7 @@ pub mod plugin;
 pub mod runtime;
 /// The Tauri-specific settings for your runtime e.g. notification permission status.
 pub mod settings;
+mod state;
 #[cfg(feature = "updater")]
 pub mod updater;
 
@@ -36,27 +38,32 @@ pub type Result<T> = std::result::Result<T, Error>;
 /// A task to run on the main thread.
 pub type SyncTask = Box<dyn FnOnce() + Send>;
 
-use crate::api::assets::Assets;
-use crate::api::config::Config;
-use crate::event::{Event, EventHandler};
-use crate::runtime::tag::{Tag, TagRef};
-use crate::runtime::window::PendingWindow;
-use crate::runtime::{Dispatch, Runtime};
+use crate::{
+  api::{assets::Assets, config::Config},
+  event::{Event, EventHandler},
+  runtime::{
+    tag::{Tag, TagRef},
+    window::PendingWindow,
+    Dispatch, Runtime,
+  },
+};
 use serde::Serialize;
-use std::collections::HashMap;
-use std::path::PathBuf;
+use std::{borrow::Borrow, collections::HashMap, path::PathBuf};
 
 // Export types likely to be used by the application.
 pub use {
-  api::config::WindowUrl,
-  hooks::{InvokeHandler, InvokeMessage, OnPageLoad, PageLoadPayload, SetupHook},
-  runtime::app::{App, Builder},
-  runtime::flavors::wry::Wry,
-  runtime::webview::{WebviewAttributes, WindowBuilder},
-  runtime::window::export::Window,
+  self::api::config::WindowUrl,
+  self::hooks::{
+    InvokeHandler, InvokeMessage, InvokeResolver, InvokeResponse, OnPageLoad, PageLoadPayload,
+    SetupHook,
+  },
+  self::runtime::app::{App, Builder},
+  self::runtime::flavors::wry::Wry,
+  self::runtime::webview::{WebviewAttributes, WindowBuilder},
+  self::runtime::window::export::Window,
+  self::state::{State, StateManager},
 };
 
-use std::borrow::Borrow;
 /// Reads the config file at compile time and generates a [`Context`] based on its content.
 ///
 /// The default config file path is a `tauri.conf.json` file inside the Cargo manifest directory of
@@ -223,6 +230,23 @@ pub trait Manager<P: Params>: sealed::ManagerBase<P> {
   fn windows(&self) -> HashMap<P::Label, Window<P>> {
     self.manager().windows()
   }
+
+  /// Add `state` to the state managed by the application.
+  /// See [`tauri::Builder#manage`] for instructions.
+  fn manage<T>(&self, state: T)
+  where
+    T: Send + Sync + 'static,
+  {
+    self.manager().state().set(state);
+  }
+
+  /// Gets the managed state for the type `T`.
+  fn state<T>(&self) -> State<'_, T>
+  where
+    T: Send + Sync + 'static,
+  {
+    self.manager().inner.state.get()
+  }
 }
 
 /// Prevent implementation details from leaking out of the [`Manager`] and [`Params`] traits.

+ 12 - 9
core/tauri/src/plugin.rs

@@ -6,8 +6,8 @@
 
 use crate::{
   api::config::PluginConfig,
-  hooks::{InvokeMessage, PageLoadPayload},
-  Params, Window,
+  hooks::{InvokeMessage, InvokeResolver, PageLoadPayload},
+  App, Params, Window,
 };
 use serde_json::Value as JsonValue;
 use std::collections::HashMap;
@@ -22,7 +22,7 @@ pub trait Plugin<M: Params>: Send {
 
   /// Initialize the plugin.
   #[allow(unused_variables)]
-  fn initialize(&mut self, config: JsonValue) -> Result<()> {
+  fn initialize(&mut self, app: &App<M>, config: JsonValue) -> Result<()> {
     Ok(())
   }
 
@@ -45,7 +45,7 @@ pub trait Plugin<M: Params>: Send {
 
   /// Add invoke_handler API extension commands.
   #[allow(unused_variables)]
-  fn extend_api(&mut self, message: InvokeMessage<M>) {}
+  fn extend_api(&mut self, message: InvokeMessage<M>, resolver: InvokeResolver<M>) {}
 }
 
 /// Plugin collection type.
@@ -70,10 +70,13 @@ impl<M: Params> PluginStore<M> {
   }
 
   /// Initializes all plugins in the store.
-  pub(crate) fn initialize(&mut self, config: &PluginConfig) -> crate::Result<()> {
+  pub(crate) fn initialize(&mut self, app: &App<M>, config: &PluginConfig) -> crate::Result<()> {
     self.store.values_mut().try_for_each(|plugin| {
       plugin
-        .initialize(config.0.get(plugin.name()).cloned().unwrap_or_default())
+        .initialize(
+          &app,
+          config.0.get(plugin.name()).cloned().unwrap_or_default(),
+        )
         .map_err(|e| crate::Error::PluginInitialization(plugin.name().to_string(), e.to_string()))
     })
   }
@@ -105,7 +108,7 @@ impl<M: Params> PluginStore<M> {
       .for_each(|plugin| plugin.on_page_load(window.clone(), payload.clone()))
   }
 
-  pub(crate) fn extend_api(&mut self, mut message: InvokeMessage<M>) {
+  pub(crate) fn extend_api(&mut self, mut message: InvokeMessage<M>, resolver: InvokeResolver<M>) {
     let command = message.command.replace("plugin:", "");
     let mut tokens = command.split('|');
     // safe to unwrap: split always has a least one item
@@ -116,9 +119,9 @@ impl<M: Params> PluginStore<M> {
         .next()
         .map(|c| c.to_string())
         .unwrap_or_else(String::new);
-      plugin.extend_api(message);
+      plugin.extend_api(message, resolver);
     } else {
-      message.reject(format!("plugin {} not found", target));
+      resolver.reject(format!("plugin {} not found", target));
     }
   }
 }

+ 65 - 6
core/tauri/src/runtime/app.rs

@@ -4,7 +4,7 @@
 
 use crate::{
   api::{assets::Assets, config::WindowUrl},
-  hooks::{InvokeHandler, InvokeMessage, OnPageLoad, PageLoadPayload, SetupHook},
+  hooks::{InvokeHandler, InvokeMessage, InvokeResolver, OnPageLoad, PageLoadPayload, SetupHook},
   plugin::{Plugin, PluginStore},
   runtime::{
     flavors::wry::Wry,
@@ -15,7 +15,7 @@ use crate::{
     Dispatch, Runtime,
   },
   sealed::{ManagerBase, RuntimeOrDispatch},
-  Context, Manager, Params, Window,
+  Context, Manager, Params, StateManager, Window,
 };
 
 use std::{collections::HashMap, sync::Arc};
@@ -126,6 +126,9 @@ where
 
   /// The webview protocols available to all windows.
   uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
+
+  /// App state.
+  state: StateManager,
 }
 
 impl<E, L, A, R> Builder<E, L, A, R>
@@ -139,18 +142,20 @@ where
   pub fn new() -> Self {
     Self {
       setup: Box::new(|_| Ok(())),
-      invoke_handler: Box::new(|_| ()),
+      invoke_handler: Box::new(|_, _| ()),
       on_page_load: Box::new(|_, _| ()),
       pending_windows: Default::default(),
       plugins: PluginStore::default(),
       uri_scheme_protocols: Default::default(),
+      state: StateManager::new(),
     }
   }
 
   /// Defines the JS message handler callback.
   pub fn invoke_handler<F>(mut self, invoke_handler: F) -> Self
   where
-    F: Fn(InvokeMessage<Args<E, L, A, R>>) + Send + Sync + 'static,
+    F:
+      Fn(InvokeMessage<Args<E, L, A, R>>, InvokeResolver<Args<E, L, A, R>>) + Send + Sync + 'static,
   {
     self.invoke_handler = Box::new(invoke_handler);
     self
@@ -180,6 +185,58 @@ where
     self
   }
 
+  /// Add `state` to the state managed by the application.
+  ///
+  /// This method can be called any number of times as long as each call
+  /// refers to a different `T`.
+  ///
+  /// Managed state can be retrieved by any request handler via the
+  /// [`State`](crate::State) request guard. In particular, if a value of type `T`
+  /// is managed by Tauri, adding `State<T>` to the list of arguments in a
+  /// request handler instructs Tauri to retrieve the managed value.
+  ///
+  /// # Panics
+  ///
+  /// Panics if state of type `T` is already being managed.
+  ///
+  /// # Example
+  ///
+  /// ```rust,ignore
+  /// use tauri::State;
+  ///
+  /// struct MyInt(isize);
+  /// struct MyString(String);
+  ///
+  /// #[tauri::command]
+  /// fn int_command(state: State<'_, MyInt>) -> String {
+  ///     format!("The stateful int is: {}", state.0)
+  /// }
+  ///
+  /// #[tauri::command]
+  /// fn string_command<'r>(state: State<'r, MyString>) {
+  ///     println!("state: {}", state.inner().0);
+  /// }
+  ///
+  /// fn main() {
+  ///     tauri::Builder::default()
+  ///         .manage(MyInt(10))
+  ///         .manage(MyString("Hello, managed state!".to_string()))
+  ///         .run(tauri::generate_context!())
+  ///         .expect("error while running tauri application");
+  /// }
+  /// ```
+  pub fn manage<T>(self, state: T) -> Self
+  where
+    T: Send + Sync + 'static,
+  {
+    let type_name = std::any::type_name::<T>();
+    if !self.state.set(state) {
+      panic!("state for type '{}' is already being managed", type_name);
+    }
+
+    self
+  }
+
   /// Creates a new webview.
   pub fn create_window<F>(mut self, label: L, url: WindowUrl, setup: F) -> Self
   where
@@ -237,6 +294,7 @@ where
       self.invoke_handler,
       self.on_page_load,
       self.uri_scheme_protocols,
+      self.state,
     );
 
     // set up all the windows defined in the config
@@ -254,13 +312,13 @@ where
       ));
     }
 
-    manager.initialize_plugins()?;
-
     let mut app = App {
       runtime: R::new()?,
       manager,
     };
 
+    app.manager.initialize_plugins(&app)?;
+
     let pending_labels = self
       .pending_windows
       .iter()
@@ -284,6 +342,7 @@ where
     app.run_updater(main_window);
 
     (self.setup)(&mut app).map_err(|e| crate::Error::Setup(e.to_string()))?;
+
     app.runtime.run();
     Ok(())
   }

+ 29 - 15
core/tauri/src/runtime/manager.rs

@@ -2,7 +2,6 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use crate::runtime::tag::TagRef;
 use crate::{
   api::{
     assets::Assets,
@@ -11,16 +10,19 @@ use crate::{
     PackageInfo,
   },
   event::{Event, EventHandler, Listeners},
-  hooks::{InvokeHandler, InvokeMessage, InvokePayload, OnPageLoad, PageLoadPayload},
+  hooks::{InvokeHandler, InvokeMessage, InvokeResolver, OnPageLoad, PageLoadPayload},
   plugin::PluginStore,
   runtime::{
-    tag::{tags_to_javascript_array, Tag, ToJsString},
-    webview::{CustomProtocol, FileDropEvent, FileDropHandler, WebviewRpcHandler, WindowBuilder},
+    tag::{tags_to_javascript_array, Tag, TagRef, ToJsString},
+    webview::{
+      CustomProtocol, FileDropEvent, FileDropHandler, InvokePayload, WebviewRpcHandler,
+      WindowBuilder,
+    },
     window::{DetachedWindow, PendingWindow},
     Icon, Runtime,
   },
   sealed::ParamsBase,
-  Context, Params, Window,
+  App, Context, Params, StateManager, Window,
 };
 use serde::Serialize;
 use serde_json::Value as JsonValue;
@@ -52,6 +54,7 @@ pub struct InnerWindowManager<P: Params> {
   windows: Mutex<HashMap<P::Label, Window<P>>>,
   plugins: Mutex<PluginStore<P>>,
   listeners: Listeners<P::Event, P::Label>,
+  pub(crate) state: Arc<StateManager>,
 
   /// The JS message handler.
   invoke_handler: Box<InvokeHandler<P>>,
@@ -67,7 +70,7 @@ pub struct InnerWindowManager<P: Params> {
   salts: Mutex<HashSet<Uuid>>,
   package_info: PackageInfo,
   /// The webview protocols protocols available to all windows.
-  uri_scheme_protocols: HashMap<String, std::sync::Arc<CustomProtocol>>,
+  uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
 }
 
 /// A [Zero Sized Type] marker representing a full [`Params`].
@@ -119,13 +122,15 @@ impl<P: Params> WindowManager<P> {
     plugins: PluginStore<P>,
     invoke_handler: Box<InvokeHandler<P>>,
     on_page_load: Box<OnPageLoad<P>>,
-    uri_scheme_protocols: HashMap<String, std::sync::Arc<CustomProtocol>>,
+    uri_scheme_protocols: HashMap<String, Arc<CustomProtocol>>,
+    state: StateManager,
   ) -> Self {
     Self {
       inner: Arc::new(InnerWindowManager {
         windows: Mutex::default(),
         plugins: Mutex::new(plugins),
         listeners: Listeners::default(),
+        state: Arc::new(state),
         invoke_handler,
         on_page_load,
         config: context.config,
@@ -144,6 +149,11 @@ impl<P: Params> WindowManager<P> {
     self.inner.windows.lock().expect("poisoned window manager")
   }
 
+  /// State managed by the application.
+  pub(crate) fn state(&self) -> Arc<StateManager> {
+    self.inner.state.clone()
+  }
+
   // setup content for dev-server
   #[cfg(dev)]
   fn get_url(&self) -> String {
@@ -383,7 +393,7 @@ impl<P: Params> WindowManager<P> {
 #[cfg(test)]
 mod test {
   use super::{Args, WindowManager};
-  use crate::{generate_context, plugin::PluginStore, runtime::flavors::wry::Wry};
+  use crate::{generate_context, plugin::PluginStore, runtime::flavors::wry::Wry, StateManager};
 
   #[test]
   fn check_get_url() {
@@ -391,9 +401,10 @@ mod test {
     let manager: WindowManager<Args<String, String, _, Wry>> = WindowManager::with_handlers(
       context,
       PluginStore::default(),
-      Box::new(|_| ()),
+      Box::new(|_, _| ()),
       Box::new(|_, _| ()),
       Default::default(),
+      StateManager::new(),
     );
 
     #[cfg(custom_protocol)]
@@ -405,9 +416,10 @@ mod test {
 }
 
 impl<P: Params> WindowManager<P> {
-  pub fn run_invoke_handler(&self, message: InvokeMessage<P>) {
-    (self.inner.invoke_handler)(message);
+  pub fn run_invoke_handler(&self, message: InvokeMessage<P>, resolver: InvokeResolver<P>) {
+    (self.inner.invoke_handler)(message, resolver);
   }
+
   pub fn run_on_page_load(&self, window: Window<P>, payload: PageLoadPayload) {
     (self.inner.on_page_load)(window.clone(), payload.clone());
     self
@@ -417,21 +429,23 @@ impl<P: Params> WindowManager<P> {
       .expect("poisoned plugin store")
       .on_page_load(window, payload);
   }
-  pub fn extend_api(&self, message: InvokeMessage<P>) {
+
+  pub fn extend_api(&self, message: InvokeMessage<P>, resolver: InvokeResolver<P>) {
     self
       .inner
       .plugins
       .lock()
       .expect("poisoned plugin store")
-      .extend_api(message);
+      .extend_api(message, resolver);
   }
-  pub fn initialize_plugins(&self) -> crate::Result<()> {
+
+  pub fn initialize_plugins(&self, app: &App<P>) -> crate::Result<()> {
     self
       .inner
       .plugins
       .lock()
       .expect("poisoned plugin store")
-      .initialize(&self.inner.config.plugins)
+      .initialize(&app, &self.inner.config.plugins)
   }
 
   pub fn prepare_window(

+ 13 - 0
core/tauri/src/runtime/webview.rs

@@ -9,6 +9,7 @@ use crate::{
   api::config::{WindowConfig, WindowUrl},
   runtime::window::DetachedWindow,
 };
+use serde::Deserialize;
 use serde_json::Value as JsonValue;
 use std::{collections::HashMap, path::PathBuf};
 
@@ -166,3 +167,15 @@ pub(crate) type WebviewRpcHandler<M> = Box<dyn Fn(DetachedWindow<M>, RpcRequest)
 /// File drop handler callback
 /// Return `true` in the callback to block the OS' default behavior of handling a file drop.
 pub(crate) type FileDropHandler<M> = Box<dyn Fn(FileDropEvent, DetachedWindow<M>) -> bool + Send>;
+
+#[derive(Deserialize)]
+pub(crate) struct InvokePayload {
+  #[serde(rename = "__tauriModule")]
+  pub(crate) tauri_module: Option<String>,
+  pub(crate) callback: String,
+  pub(crate) error: String,
+  #[serde(rename = "mainThread", default)]
+  pub(crate) main_thread: bool,
+  #[serde(flatten)]
+  pub(crate) inner: JsonValue,
+}

+ 31 - 7
core/tauri/src/runtime/window.rs

@@ -7,10 +7,10 @@
 use crate::{
   api::config::WindowConfig,
   event::{Event, EventHandler},
-  hooks::{InvokeMessage, InvokePayload, PageLoadPayload},
+  hooks::{InvokeMessage, InvokeResolver, PageLoadPayload},
   runtime::{
     tag::ToJsString,
-    webview::{FileDropHandler, WebviewAttributes, WebviewRpcHandler},
+    webview::{FileDropHandler, InvokePayload, WebviewAttributes, WebviewRpcHandler},
     Dispatch, Runtime,
   },
   sealed::{ManagerBase, RuntimeOrDispatch},
@@ -114,6 +114,7 @@ impl<M: Params> PartialEq for DetachedWindow<M> {
 /// We want to export the runtime related window at the crate root, but not look like a re-export.
 pub(crate) mod export {
   use super::*;
+  use crate::command::FromCommand;
   use crate::runtime::{manager::WindowManager, tag::TagRef};
   use std::borrow::Borrow;
 
@@ -166,6 +167,16 @@ pub(crate) mod export {
     }
   }
 
+  impl<'de, P: Params> FromCommand<'de, P> for Window<P> {
+    fn from_command(
+      _: &'de str,
+      _: &'de str,
+      message: &'de InvokeMessage<P>,
+    ) -> Result<Self, serde_json::Error> {
+      Ok(message.window())
+    }
+  }
+
   impl<P: Params> Window<P> {
     /// Create a new window that is attached to the manager.
     pub(crate) fn new(manager: WindowManager<P>, window: DetachedWindow<P>) -> Self {
@@ -186,14 +197,27 @@ pub(crate) mod export {
           manager.run_on_page_load(self, payload);
         }
         _ => {
-          let message = InvokeMessage::new(self, command.to_string(), payload);
-          if let Some(module) = &message.payload.tauri_module {
+          let message = InvokeMessage::new(
+            self.clone(),
+            manager.state(),
+            command.to_string(),
+            payload.inner,
+          );
+          let resolver =
+            InvokeResolver::new(self, payload.main_thread, payload.callback, payload.error);
+          if let Some(module) = &payload.tauri_module {
             let module = module.to_string();
-            crate::endpoints::handle(module, message, manager.config(), manager.package_info());
+            crate::endpoints::handle(
+              module,
+              message,
+              resolver,
+              manager.config(),
+              manager.package_info(),
+            );
           } else if command.starts_with("plugin:") {
-            manager.extend_api(message);
+            manager.extend_api(message, resolver);
           } else {
-            manager.run_invoke_handler(message);
+            manager.run_invoke_handler(message, resolver);
           }
         }
       }

+ 63 - 0
core/tauri/src/state.rs

@@ -0,0 +1,63 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use crate::command::FromCommand;
+use crate::{InvokeMessage, Params};
+use state::Container;
+
+/// A guard for a state value.
+pub struct State<'r, T: Send + Sync + 'static>(&'r T);
+
+impl<'r, T: Send + Sync + 'static> State<'r, T> {
+  /// Retrieve a borrow to the underlying value with a lifetime of `'r`.
+  /// Using this method is typically unnecessary as `State` implements
+  /// [`Deref`] with a [`Deref::Target`] of `T`.
+  #[inline(always)]
+  pub fn inner(&self) -> &'r T {
+    self.0
+  }
+}
+
+impl<T: Send + Sync + 'static> std::ops::Deref for State<'_, T> {
+  type Target = T;
+
+  #[inline(always)]
+  fn deref(&self) -> &T {
+    self.0
+  }
+}
+
+impl<T: Send + Sync + 'static> Clone for State<'_, T> {
+  fn clone(&self) -> Self {
+    State(self.0)
+  }
+}
+
+impl<'r, 'de: 'r, T: Send + Sync + 'static, P: Params> FromCommand<'de, P> for State<'r, T> {
+  fn from_command(
+    _: &'de str,
+    _: &'de str,
+    message: &'de InvokeMessage<P>,
+  ) -> Result<Self, serde_json::Error> {
+    Ok(message.state.get())
+  }
+}
+
+/// The Tauri state manager.
+pub struct StateManager(pub(crate) Container);
+
+impl StateManager {
+  pub(crate) fn new() -> Self {
+    Self(Container::new())
+  }
+
+  pub(crate) fn set<T: Send + Sync + 'static>(&self, state: T) -> bool {
+    self.0.set(state)
+  }
+
+  /// Gets the state associated with the specified type.
+  pub fn get<T: Send + Sync + 'static>(&self) -> State<'_, T> {
+    State(self.0.get())
+  }
+}

+ 2 - 6
examples/api/src-tauri/src/cmd.rs

@@ -11,12 +11,8 @@ pub struct RequestBody {
   name: String,
 }
 
-#[command(with_window)]
-pub fn log_operation<M: tauri::Params>(
-  _window: tauri::Window<M>,
-  event: String,
-  payload: Option<String>,
-) {
+#[command]
+pub fn log_operation(event: String, payload: Option<String>) {
   println!("{} {:?}", event, payload);
 }
 

+ 7 - 0
examples/commands/package.json

@@ -0,0 +1,7 @@
+{
+  "name": "commands",
+  "version": "1.0.0",
+  "scripts": {
+    "tauri": "node ../../tooling/cli.js/bin/tauri"
+  }
+}

+ 55 - 0
examples/commands/public/index.html

@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8" />
+  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+  <title>Tauri</title>
+</head>
+
+<body>
+  <h1>Tauri Commands</h1>
+  <div id="response">Response:</div>
+  <div id="container"></div>
+  <script>
+    const responseDiv = document.querySelector('#response')
+    function runCommand(commandName, args) {
+      window.__TAURI__.invoke(commandName, args).then(response => {
+        responseDiv.innerHTML = `Response: Ok(${response})`
+      }).catch(error => {
+        responseDiv.innerHTML = `Response: Err(${error})`
+      })
+    }
+
+    const container = document.querySelector('#container')
+    const commands = [
+      { 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 },
+    ]
+
+    for (command of commands) {
+      const { name, required } = command
+      const button = document.createElement('button')
+      button.innerHTML = `Run ${name}`;
+      button.addEventListener("click", function () {
+        runCommand(name, { argument: 'value' })
+        if (!required) {
+          setTimeout(() => {
+            runCommand(name, {})
+          }, 1000)
+        }
+      });
+      container.appendChild(button);
+    }
+
+  </script>
+</body>
+
+</html>

+ 10 - 0
examples/commands/src-tauri/.gitignore

@@ -0,0 +1,10 @@
+# Generated by Cargo
+# will have compiled files and executables
+/target/
+WixTools
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
+
+config.json
+bundle.json

+ 1 - 0
examples/commands/src-tauri/.license_template

@@ -0,0 +1 @@
+../../../.license_template

+ 17 - 0
examples/commands/src-tauri/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "commands"
+version = "0.1.0"
+description = "A simple Tauri Application showcasing the commands API"
+edition = "2018"
+
+[build-dependencies]
+tauri-build = { path = "../../../core/tauri-build", features = [ "codegen" ]}
+
+[dependencies]
+serde_json = "1.0"
+serde = { version = "1.0", features = [ "derive" ] }
+tauri = { path = "../../../core/tauri", features =["api-all"]}
+
+[features]
+default = [ "custom-protocol" ]
+custom-protocol = [ "tauri/custom-protocol" ]

+ 7 - 0
examples/commands/src-tauri/build.rs

@@ -0,0 +1,7 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+fn main() {
+  tauri_build::build();
+}

BIN
examples/commands/src-tauri/icons/128x128.png


BIN
examples/commands/src-tauri/icons/128x128@2x.png


BIN
examples/commands/src-tauri/icons/32x32.png


BIN
examples/commands/src-tauri/icons/Square107x107Logo.png


BIN
examples/commands/src-tauri/icons/Square142x142Logo.png


BIN
examples/commands/src-tauri/icons/Square150x150Logo.png


BIN
examples/commands/src-tauri/icons/Square284x284Logo.png


BIN
examples/commands/src-tauri/icons/Square30x30Logo.png


BIN
examples/commands/src-tauri/icons/Square310x310Logo.png


BIN
examples/commands/src-tauri/icons/Square44x44Logo.png


BIN
examples/commands/src-tauri/icons/Square71x71Logo.png


BIN
examples/commands/src-tauri/icons/Square89x89Logo.png


BIN
examples/commands/src-tauri/icons/StoreLogo.png


BIN
examples/commands/src-tauri/icons/icon.icns


BIN
examples/commands/src-tauri/icons/icon.ico


BIN
examples/commands/src-tauri/icons/icon.png


+ 92 - 0
examples/commands/src-tauri/src/main.rs

@@ -0,0 +1,92 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+#![cfg_attr(
+  all(not(debug_assertions), target_os = "windows"),
+  windows_subsystem = "windows"
+)]
+
+#[derive(Debug)]
+struct MyState {
+  value: u64,
+  label: String,
+}
+
+#[tauri::command]
+fn simple_command(argument: String) {
+  println!("{}", argument);
+}
+
+#[tauri::command]
+fn stateful_command(argument: Option<String>, state: tauri::State<'_, MyState>) {
+  println!("{:?} {:?}", argument, state.inner());
+}
+
+// Async commands
+
+#[tauri::command]
+async fn async_simple_command(argument: String) {
+  println!("{}", argument);
+}
+
+#[tauri::command]
+async fn async_stateful_command(argument: Option<String>, state: tauri::State<'_, MyState>) {
+  println!("{:?} {:?}", argument, state.inner());
+}
+
+// ------------------------ Commands returning Result ------------------------
+
+type Result<T> = std::result::Result<T, ()>;
+
+#[tauri::command]
+fn simple_command_with_result(argument: String) -> Result<String> {
+  println!("{}", argument);
+  Ok(argument)
+}
+
+#[tauri::command]
+fn stateful_command_with_result(
+  argument: Option<String>,
+  state: tauri::State<'_, MyState>,
+) -> Result<String> {
+  println!("{:?} {:?}", argument, state.inner());
+  Ok(argument.unwrap_or_else(|| "".to_string()))
+}
+
+// Async commands
+
+#[tauri::command]
+async fn async_simple_command_with_result(argument: String) -> Result<String> {
+  println!("{}", argument);
+  Ok(argument)
+}
+
+#[tauri::command]
+async fn async_stateful_command_with_result(
+  argument: Option<String>,
+  state: tauri::State<'_, MyState>,
+) -> Result<String> {
+  println!("{:?} {:?}", argument, state.inner());
+  Ok(argument.unwrap_or_else(|| "".to_string()))
+}
+
+fn main() {
+  tauri::Builder::default()
+    .manage(MyState {
+      value: 0,
+      label: "Tauri!".into(),
+    })
+    .invoke_handler(tauri::generate_handler![
+      simple_command,
+      stateful_command,
+      async_simple_command,
+      async_stateful_command,
+      simple_command_with_result,
+      stateful_command_with_result,
+      async_simple_command_with_result,
+      async_stateful_command_with_result
+    ])
+    .run(tauri::generate_context!())
+    .expect("error while running tauri application");
+}

+ 56 - 0
examples/commands/src-tauri/tauri.conf.json

@@ -0,0 +1,56 @@
+{
+  "build": {
+    "distDir": "../public",
+    "devPath": "../public",
+    "beforeDevCommand": "",
+    "beforeBuildCommand": ""
+  },
+  "tauri": {
+    "bundle": {
+      "active": true,
+      "targets": "all",
+      "identifier": "com.tauri.dev",
+      "icon": [
+        "icons/32x32.png",
+        "icons/128x128.png",
+        "icons/128x128@2x.png",
+        "icons/icon.icns",
+        "icons/icon.ico"
+      ],
+      "resources": [],
+      "externalBin": [],
+      "copyright": "",
+      "category": "DeveloperTool",
+      "shortDescription": "",
+      "longDescription": "",
+      "deb": {
+        "depends": [],
+        "useBootstrapper": false
+      },
+      "macOS": {
+        "frameworks": [],
+        "minimumSystemVersion": "",
+        "useBootstrapper": false,
+        "exceptionDomain": ""
+      }
+    },
+    "allowlist": {
+      "all": true
+    },
+    "windows": [
+      {
+        "title": "Welcome to Tauri!",
+        "width": 800,
+        "height": 600,
+        "resizable": true,
+        "fullscreen": false
+      }
+    ],
+    "security": {
+      "csp": "default-src blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'"
+    },
+    "updater": {
+      "active": false
+    }
+  }
+}