taurpc 0.8.2

A type-safe IPC layer for tauri commands
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# TauRPC

[![](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/crates/l/taurpc)

This package is a Tauri extension to give you a fully-typed IPC layer for [Tauri commands](https://v2.tauri.app/develop/calling-rust/#commands) and [events](https://v2.tauri.app/develop/calling-rust/#event-system).

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](https://github.com/oscartbeaumont/specta) is used under the hood for the type-generation. The trait-based API structure was inspired by [tarpc](https://github.com/google/tarpc).

# Usage🔧

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

```toml
# src-tauri/Cargo.toml

[dependencies]
taurpc = "0.8.2"

specta = { version = "=2.0.0-rc.25", features = ["derive"] }
# specta-typescript = "0.0.12"
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](https://github.com/MatsDK/TauRPC/#calling-the-frontend)

```rust
// 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() {
    let api_handler = ApiImpl.into_handler();

    #[cfg(debug_assertions)]
    taurpc::Exporter::new()
        .export(&api_handler, "../src/bindings.ts")
        .unwrap();

    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(taurpc::create_ipc_handler(api_handler))
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

The `#[taurpc::procedures]` trait will generate everything necessary for handling calls. To generate and export TypeScript bindings, use the `taurpc::Exporter` builder. Usually, you only want to generate bindings in debug builds, so you wrap it in `#[cfg(debug_assertions)]`.
Once you start your application (e.g. by running `pnpm tauri dev`), the TS types will be generated at the specified path.

Then on the frontend install the taurpc package.

```bash
pnpm install taurpc
```

Now on the frontend you import the proxy creator from the generated file. With these types a typesafe proxy is generated that you can use to invoke commands and listen for events.

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

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

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](https://github.com/MatsDK/TauRPC/tree/main/example).

# Using structs

If you want to use 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`.

```rust
#[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](https://doc.rust-lang.org/std/sync/struct.Mutex.html).

You can use the `window`, `app_handle` and `webview_window` arguments just like with Tauri's commands. [Tauri docs](https://v2.tauri.app/develop/calling-rust/#accessing-the-webviewwindow-in-commands)

```rust
// 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. By default, this keeps TauRPC's current behavior: promises reject on error and throw on the frontend.
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 `String`s. For more complex scenarios, you can create your own error type that implements `serde::Serialize`.
You can find an example using [thiserror](https://github.com/dtolnay/thiserror) [here](https://github.com/MatsDK/TauRPC/blob/main/example/src-tauri/src/main.rs).
You can also find more information about this in the [Tauri guides](https://v2.tauri.app/develop/calling-rust/#error-handling).

If you want type-safe errors on the frontend, you can opt in globally on the exporter:

```rust
#[cfg(debug_assertions)]
taurpc::Exporter::new()
    .error_handling(taurpc::ErrorHandlingMode::Result)
    .export(&router, "../src/bindings.ts")
    .unwrap();
```

In this mode, procedures returning `Result<T, E>` are typed as:

```ts
Promise<{ status: 'ok'; data: T } | { status: 'error'; error: E }>
```

You can also provide a custom `typedError` runtime implementation:

```rust
const TYPED_ERROR_IMPL: &str = r#"async function typedError(result) {
  try {
    return { status: "ok", data: await result };
  } catch (e) {
    if (e instanceof Error) throw e;
    return { status: "error", error: e };
  }
}"#;

#[cfg(debug_assertions)]
taurpc::Exporter::new()
    .error_handling(taurpc::ErrorHandlingMode::Result)
    .typed_error_impl(TYPED_ERROR_IMPL)
    .export(&router, "../src/bindings.ts")
    .unwrap();
```

# 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.

```rust
#[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.

```rust
// 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 = taurpc::Router::new()
        .merge(ApiImpl.into_handler())
        .merge(EventsImpl.into_handler());

    #[cfg(debug_assertions)]
    taurpc::Exporter::new()
        .export(&router, "../src/bindings.ts")
        .unwrap();

    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.

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

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

# Typescript export configuration

You can specify a custom `specta_typescript` configuration on `taurpc::Exporter`. These options will overwrite `Specta`'s defaults. Make sure to install the latest version of `specta_typescript`.
All available options can be found in [specta_typescript's docs](https://docs.rs/specta-typescript/latest/specta_typescript/struct.Typescript.html).

```rust
#[cfg(debug_assertions)]
taurpc::Exporter::new()
    .ts_config(
        specta_typescript::Typescript::default()
            .header("// My custom header")
    )
    .export(&router, "../src/bindings.ts")
    .unwrap();
```

TauRPC currently exports Rust bigint-like integers (`i64`, `u64`, `i128`, `u128`, `isize`, `usize`) as TypeScript `number` values. This keeps the generated bindings simple, but values outside JavaScript's safe integer range can lose precision.

# Calling the frontend

Trigger [events](https://v2.tauri.app/develop/calling-rust/#event-system) 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](https://github.com/MatsDK/TauRPC/blob/main/example/src-tauri/src/main.rs).

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

```rust
// 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:

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

// Run this inside a cleanup function, for example within useEffect 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:

```rust
use taurpc::Windows;

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

# Using channels

TauRPC will also generate types if you are using [Tauri Channels](https://v2.tauri.app/develop/calling-frontend/#channels).
On the frontend you will be able to pass a typed callback function to your command.

```rust
#[taurpc::ipc_type]
struct Update {
    progress: u8,
}

#[taurpc::procedures]
trait Api {
    async fn update(on_event: Channel<Update>);
}

#[derive(Clone)]
struct ApiImpl;

#[taurpc::resolvers]
impl Api for ApiImpl {
    async fn update(self, on_event: Channel<Update>) {
        for progress in [15, 20, 35, 50, 90] {
            on_event.send(Update { progress }).unwrap();
        }
    }
}
```

Calling the command:

```typescript
let taurpc = createTauRPCProxy()
await taurpc.update((update) => {
  console.log(update.progress)
})
```

# 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👀]https://blog.rust-lang.org/inside-rust/2023/05/03/stabilizing-async-fn-in-trait.html
  - [ ] Allow sync methods
- [x] Calling the frontend
- [x] Renaming event trigger struct
- [x] Send event to specific window
- [ ] React/Svelte handlers