npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

taurpc

v1.6.0

Published

[![](https://img.shields.io/npm/v/taurpc)](https://www.npmjs.com/package/taurpc) [![](https://img.shields.io/crates/v/taurpc)](https://crates.io/crates/taurpc) [![](https://img.shields.io/docsrs/taurpc)](https://docs.rs/taurpc/) ![](https://img.shields.io

Downloads

287

Readme

TauRPC

This package is a Tauri extension to give you a fully-typed IPC layer for Tauri commands and events.

The TS types corresponding to your pre-defined Rust backend API are generated on runtime, after which they can be used to call the backend from your TypeScript frontend framework of choice. This crate provides typesafe bidirectional IPC communication between the Rust backend and TypeScript frontend. Specta is used under the hood for the type-generation. The trait-based API structure was inspired by tarpc.

Usage🔧

First, add the following crates to your Cargo.toml:

# src-tauri/Cargo.toml

[dependencies]
taurpc = "0.3.0"

specta = { version = "=2.0.0-rc.9", features = ["export"] }
tokio = { version = "1", features = ["full"] }

Then, declare and implement your IPC methods and resolvers. If you want to use your API for Tauri's events, you don't have to implement the resolvers, go to Calling the frontend

// src-tauri/src/main.rs

#[taurpc::procedures]
trait Api {
    async fn hello_world();
}

#[derive(Clone)]
struct ApiImpl;

#[taurpc::resolvers]
impl Api for ApiImpl {
    async fn hello_world(self) {
        println!("Hello world");
    }
}

#[tokio::main]
async fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(taurpc::create_ipc_handler(ApiImpl.into_handler()))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The #[taurpc::procedures] trait will generate everything necessary for handling calls and the type-generation. Now, you should run pnpm tauri dev to generate and export the TS types. The types will by default be exported to bindings.ts in the root of your project, but you can specify an export path by doing this #[taurpc::procedures(export_to = "../src/types.ts")].

Then on the frontend install the taurpc package.

pnpm install taurpc@next

Now on the frontend you import the generated types, if you specified the export_to attribute on your procedures you should import your from there. With these types a typesafe proxy is generated that you can use to invoke commands and listen for events.

import { createTauRPCProxy } from '../bindings.ts'

const taurpc = await createTauRPCProxy()
await taurpc.hello_world()

The types for taurpc are generated once you start your application, run pnpm tauri dev. If the types are not picked up by the LSP, you may have to restart typescript to reload the types.

You can find a complete example (using Svelte) here.

Using structs

If you want to you structs for the inputs/outputs of procedures, you should always add #[taurpc::ipc_type] to make sure the coresponding ts types are generated. This make will derive serde Serialize and Deserialize, Clone and specta::Type.

#[taurpc::ipc_type]
// #[derive(serde::Serialize, serde::Deserialize, specta::Type, Clone)]
struct User {
    user_id: u32,
    first_name: String,
    last_name: String,
}

#[taurpc::procedures]
trait Api {
    async fn get_user() -> User;
}

Accessing managed state

To share some state between procedures, you can add fields on the API implementation struct. If the state requires to be mutable, you need to use a container that enables interior mutability, like a Mutex.

You can use the window and app_handle arguments just like with Tauri's commands. Tauri docs

// src-tauri/src/main.rs

use std::sync::Arc;
use tokio::sync::Mutex;
use tauri::{Manager, Runtime, State, Window};

type MyState = Arc<Mutex<String>>;

#[taurpc::procedures]
trait Api {
    async fn with_state();

    async fn with_window<R: Runtime>(window: Window<R>);
}

#[derive(Clone)]
struct ApiImpl {
    state: MyState
};

#[taurpc::resolvers]
impl Api for ApiImpl {
    async fn with_state(self) {
        // ... 
        // let state = self.state.lock().await;
        // ... 
    }

    async fn with_window<R: Runtime>(self, window: Window<R>) {
        // ...
    }
}

#[tokio::main]
async fn main() {
    tauri::Builder::default()
        .invoke_handler(taurpc::create_ipc_handler(
            ApiImpl {
                state: Arc::new(Mutex::new("state".to_string())),
            }
            .into_handler(),
        ))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Custom error handling

You can return a Result<T, E> to return an error if the procedure fails. This is will reject the promise on the frontend and throw an error. If you're working with error types from Rust's std library, they will probably not implement serde::Serialize which is required for anything that is returned in the procedure. In simple scenarios you can use map_err to convert these errors to Strings. For more complex scenarios, you can create your own error type that implements serde::Serialize. You can find an example using thiserror here. You can also find more information about this in the Tauri guides.

Extra options for procedures

Inside your procedures trait you can add attributes to the defined methods. This can be used to ignore or rename a method. Renaming will change the name of the procedure on the frontend.

#[taurpc::procedures]
trait Api {
    // #[taurpc(skip)]
    #[taurpc(alias = "_hello_world_")]
    async fn hello_world();
}

Routing

It is possible to define all your commands and events inside a single procedures trait, but this can quickly get cluttered. By using the Router struct you can create nested commands and events, that you can call using a proxy TypeScript client.

The path of the procedures trait is set by using the path attribute on #[taurpc::procedures(path = "")], then you can create an empty router and use the merge method to add handlers to the router. You can only have 1 trait without a path specified, this will be the root. Finally instead of using taurpc::create_ipc_handler(), you should just call into_handler() on the router.

// Root procedures
#[taurpc::procedures]
trait Api {
    async fn hello_world();
}

#[derive(Clone)]
struct ApiImpl;

#[taurpc::resolvers]
impl Api for ApiImpl {
    async fn hello_world(self) {
        println!("Hello world");
    }
}

// Nested procedures, you can also do this (path = "api.events.users")
#[taurpc::procedures(path = "events")]
trait Events {
    #[taurpc(event)]
    async fn event();
}

#[derive(Clone)]
struct EventsImpl;

#[taurpc::resolvers]
impl Events for EventsImpl {}

#[tokio::main]
async fn main() {
    let router = Router::new()
        .merge(ApiImpl.into_handler())
        .merge(EventsImpl.into_handler());

    tauri::Builder::default()
        .invoke_handler(router.into_handler())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Now on the frontend you can use the proxy client.

// Call `hello_world` on the root layer
await taurpc.hello_world()

// Listen for `event` on the `events` layer
const unlisten = taurpc.events.event.on(() => {
  console.log('Hello World!')
})

Calling the frontend

Trigger events on your TypeScript frontend from your Rust backend with a fully-typed experience. The #[taurpc::procedures] macro also generates a struct that you can use to trigger the events, this means you can define the event types the same way you define the procedures.

First start by declaring the API structure, by default the event trigger struct will be identified by TauRpc{trait_ident}EventTrigger. If you want to change this, you can add an attribute to do this, #[taurpc::procedures(event_trigger = ApiEventTrigger)]. For more details you can look at the example.

You should add the #[taurpc(event)] attribute to your events. If you do this, you will not have to implement the corresponding resolver.

// src-tauri/src/main.rs

#[taurpc::procedures(event_trigger = ApiEventTrigger)]
trait Api {
    #[taurpc(event)]
    async fn hello_world();
}

#[derive(Clone)]
struct ApiImpl;

#[taurpc::resolvers]
impl Api for ApiImpl {}

#[tokio::main]
async fn main() {
    tauri::Builder::default()
        .invoke_handler(taurpc::create_ipc_handler(ApiImpl.into_handler()))
        .setup(|app| {
            let trigger = ApiEventTrigger::new(app.handle());
            trigger.hello_world()?;

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Then, on the frontend you can listen for the events with types:

const unlisten = taurpc.hello_world.on(() => {
  console.log('Hello World!')
})

// Run this inside a cleanup function, for example in React and onDestroy in Svelte
unlisten()

Sending an event to a specific window

By default, events are emitted to all windows. If you want to send an event to a specific window by label, you can do the following:

use taurpc::Windows;

trigger.send_to(Windows::One("main".to_string())).hello_world()?;
// Options:
//   - Windows::All (default)
//   - Windows::One(String)
//   - Windows::N(Vec<String>)

Features

  • [x] Basic inputs
  • [x] Struct inputs
  • [x] Sharing state
    • [ ] Use Tauri's managed state?
  • [x] Renaming methods
  • [x] Nested routes
  • [x] Merging routers
  • [x] Custom error handling
  • [x] Typed outputs
  • [x] Async methods - async traits👀
    • [ ] Allow sync methods
  • [x] Calling the frontend
  • [x] Renaming event trigger struct
  • [x] Send event to specific window
  • [ ] React/Svelte handlers