tauri-remote-ui 0.28.0 β 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +39 -21
- package/README.md +59 -103
- package/api/core/index.cjs +30 -36
- package/api/core/index.d.ts +8 -0
- package/api/core/index.js +31 -37
- package/api/event/index.cjs +17 -33
- package/api/event/index.d.ts +3 -2
- package/api/event/index.js +15 -34
- package/package.json +6 -5
- package/socket.cjs +114 -54
- package/socket.d.ts +28 -9
- package/socket.js +112 -55
- package/version.cjs +13 -0
- package/version.d.ts +9 -0
- package/version.js +11 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
# License Information
|
|
2
|
+
|
|
3
|
+
This project is licensed under **AGPL-3.0** starting from version 1.x.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## π Open Source (AGPL-3.0)
|
|
8
|
+
|
|
9
|
+
- You are free to use, study, and modify this project under the terms of the **GNU Affero General Public License v3.0 (AGPL-3.0)**.
|
|
10
|
+
- **Important:** If you build on top of this project (derivative works, forks, integrations, etc.) you must also release your work under **AGPL-3.0**.
|
|
11
|
+
- This ensures improvements and extensions remain open to the community.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## πΌ Commercial Use (Sponsorware Model)
|
|
16
|
+
|
|
17
|
+
- Commercial use **is not allowed** under AGPL-3.0 without a separate license.
|
|
18
|
+
- To use this project in **commercial products, SaaS, internal tools, or client projects**, your organization must:
|
|
19
|
+
1. **Sponsor this project** at the required tier on [GitHub Sponsors](https://github.com/sponsors/DraviaVemal).
|
|
20
|
+
2. Sponsored organizations are granted access to a **private repository**, licensed under a **commercial-friendly license (MIT terms)**.
|
|
21
|
+
3. This private repo contains the same core code under commercial terms, plus optional add-ons.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## π Sponsor Benefits
|
|
26
|
+
|
|
27
|
+
- Access to a **commercial license** for this project (MIT terms).
|
|
28
|
+
- Access to the **private repo** with code updates.
|
|
29
|
+
- **Priority support** β issues and feature requests in the private repo are handled **before public requests**.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## β
How to Comply
|
|
34
|
+
|
|
35
|
+
- **Individuals, students, hobbyists** β Use under **AGPL-3.0** (non-commercial OK).
|
|
36
|
+
- **Businesses, organizations, contractors** β Must **sponsor** to obtain commercial rights.
|
|
37
|
+
|
|
38
|
+
For details on AGPL-3.0, see [LICENSE](https://opensource.org/license/agpl-v3).
|
|
39
|
+
For commercial licensing inquiries, contact: **contact@draviavemal.com**.
|
package/README.md
CHANGED
|
@@ -1,109 +1,65 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tauri-remote-ui (AGPL-3.0)
|
|
2
2
|
|
|
3
|
-
**Tauri Remote UI** is a plugin that allows you to expose your Tauri application's UI to any web browser
|
|
3
|
+
**Tauri Remote UI** is a plugin that allows you to expose your Tauri application's UI to any web browser.
|
|
4
4
|
|
|
5
5
|
## Badges
|
|
6
|
+
|
|
6
7
|
 
|
|
7
8
|
|
|
9
|
+
## π License
|
|
10
|
+
|
|
11
|
+
- Open Source: AGPL-3.0 (see LICENSE)
|
|
12
|
+
- Commercial: Available via sponsorship (see LICENSE)
|
|
13
|
+
|
|
14
|
+
## β¨ Features
|
|
15
|
+
|
|
16
|
+
- Seamless enable/diable integration
|
|
17
|
+
- Network level access control setting
|
|
18
|
+
- Network latency tracking
|
|
19
|
+
|
|
20
|
+
## β οΈ Security Warning β Read Before Exposing the Server
|
|
21
|
+
|
|
22
|
+
> **This plugin currently ships with NO authentication, NO authorization, and NO transport encryption.**
|
|
23
|
+
> Anyone who can reach the bound port can drive your application's UI and invoke any Tauri command your app exposes β there is no login, no API key, no TLS, and no rate limit.
|
|
24
|
+
|
|
25
|
+
The only access control today is a coarse **network-scope filter** (`OriginType`) applied to the peer's IP at TCP-accept time:
|
|
26
|
+
|
|
27
|
+
| Scope | Bind address | Who can connect |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| `Localhost` *(default, recommended)* | `127.0.0.1` | This machine only. |
|
|
30
|
+
| `Subnet` | `0.0.0.0` | Any host on **any** of this machine's local IPv4/IPv6 subnets, plus loopback. |
|
|
31
|
+
| `Any` | `0.0.0.0` | **Anyone routable to this machine.** No filter applied. |
|
|
32
|
+
|
|
33
|
+
A built-in token / SSL / SSO story is on the roadmap (see *Planned Features*), but until it lands, **the only safe public-network deployment is one where you have added authentication yourself in front of this plugin** (mutual-TLS reverse proxy, WireGuard, Tailscale, Cloudflare Access, etc.).
|
|
34
|
+
|
|
35
|
+
To audit who is being allowed through at runtime, initialize a logger in your host app and run with `RUST_LOG=tauri_remote_ui=debug` β every accept/reject decision is logged with the peer IP and the active scope.
|
|
36
|
+
|
|
37
|
+
## Supports
|
|
38
|
+
|
|
39
|
+
|Environment|Support|
|
|
40
|
+
|-|-|
|
|
41
|
+
|Windows|β
|
|
|
42
|
+
|Mac|β
|
|
|
43
|
+
|Linux|β
|
|
|
44
|
+
|Android|β|
|
|
45
|
+
|iOS|β|
|
|
46
|
+
|
|
47
|
+
## Use Case
|
|
48
|
+
|
|
49
|
+
- Enabling seamless remote interaction for development debuggingβwithout modifying your app's logic.
|
|
50
|
+
- Enabling end-to-end testing of tauri application using standard web automation tools like playwright.
|
|
51
|
+
- Note : Based on tauri target OS the webkit will change windows will be 100% match test case as both are chromium rest of OS around maximum 10% UI difference are expected from actual application
|
|
52
|
+
- Enable remote access feature for local close ciruit hardware related application
|
|
53
|
+
|
|
54
|
+
## Planned Features
|
|
55
|
+
|
|
56
|
+
- Multiple Window of Tauri app support in Remote UI logic
|
|
57
|
+
- SSO Ingration Option
|
|
58
|
+
- Custom Starting window name options
|
|
59
|
+
- Dynamic Port mapping
|
|
60
|
+
- SSL Certificate ingration
|
|
61
|
+
- Authendication system for remote access (User_id,Password)
|
|
62
|
+
|
|
63
|
+
## [Documents](https://docs.draviavemal.com)
|
|
8
64
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- **Remote UI Exposure:** Interact with your Tauri app from any browser.
|
|
12
|
-
- **Seamless Development:** Enable Development debug attachment for fronend debugging.
|
|
13
|
-
- **Seamless E2E Testing:** Use existing web automation/testing tools.
|
|
14
|
-
- **Automatic Transport Switching:** IPC for WebView, WebSocket for browsersβhandled transparently.
|
|
15
|
-
- **Customizable Security:** Control and secure remote access as needed.
|
|
16
|
-
- **Future Compatibility For Test Migration:** When [CEF-RS](https://github.com/tauri-apps/cef-rs) becomes available, the same E2E tests (e.g., written with Playwright or similar tools that use the Chromium debug port) will work seamlessly in debug mode, ensuring long-term support for modern testing workflows.
|
|
17
|
-
|
|
18
|
-
## Completed Features
|
|
19
|
-
|
|
20
|
-
### Javascript
|
|
21
|
-
- **api/core** - `invoke`
|
|
22
|
-
- **api/event** - `listen`
|
|
23
|
-
- **api/app**
|
|
24
|
-
- `defaultWindowIcon`,`fetchDataStoreIdentifiers`,`getBundleType`,
|
|
25
|
-
- `getIdentifier`,`getName`,`getTauriVersion`,
|
|
26
|
-
- `getVersion`,`hide`,`removeDataStore`,
|
|
27
|
-
- `setDockVisibility`,`setTheme`,`show`
|
|
28
|
-
|
|
29
|
-
### Rust
|
|
30
|
-
- `emit` - Emit method is updated to handle in this plugin.
|
|
31
|
-
|
|
32
|
-
## Operation Flow
|
|
33
|
-
|
|
34
|
-
- **WebView:** Uses IPC for communication between frontend and backend.
|
|
35
|
-
- **Commercial Browser:** Uses WebSocket (WS) for remote frontend-backend communication.
|
|
36
|
-
- **Automatic Switching:** The Rust backend plugin and npm frontend wrapper handle transport selection automatically.
|
|
37
|
-
- **Security:** The exposure of the web app can be secured and customized by the end user.
|
|
38
|
-
|
|
39
|
-
## Usage
|
|
40
|
-
|
|
41
|
-
1. **Install the Rust plugin** in your Tauri project `cargo add tauri-remote-ui`.
|
|
42
|
-
2. **Initialize the Rust plugin**
|
|
43
|
-
```rust
|
|
44
|
-
pub fn run() {
|
|
45
|
-
tauri::Builder::default()
|
|
46
|
-
.plugin(tauri_remote_ui::init())
|
|
47
|
-
.invoke_handler(tauri::generate_handler![
|
|
48
|
-
increment,
|
|
49
|
-
decrement,
|
|
50
|
-
enable_server,
|
|
51
|
-
disable_server,
|
|
52
|
-
exit_app,
|
|
53
|
-
])
|
|
54
|
-
.setup(|app| {
|
|
55
|
-
app.manage(Arc::new(RwLock::new(Counter { now: 0 })));
|
|
56
|
-
Ok(())
|
|
57
|
-
})
|
|
58
|
-
.run(tauri::generate_context!())
|
|
59
|
-
.expect("error while running tauri application");
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
3. **Replace Emitter trait**
|
|
63
|
-
```rust
|
|
64
|
-
use tauri::Emitter;
|
|
65
|
-
webview_window.emit(data)
|
|
66
|
-
```
|
|
67
|
-
To
|
|
68
|
-
```rust
|
|
69
|
-
use tauri_remote_ui::EmitterExt;
|
|
70
|
-
webview_window.emit(data).await
|
|
71
|
-
```
|
|
72
|
-
4. **Start/Stop Server**
|
|
73
|
-
```rust
|
|
74
|
-
async fn enable_server(app: AppHandle) -> String {
|
|
75
|
-
match app.start_remote_ui(RemoteUiConfig::default().set_port(Some(9090))).await {
|
|
76
|
-
Ok(()) => format!("Server Started."),
|
|
77
|
-
Err(err) => format!("Server Error {:?}", err),
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async fn disable_server(app: AppHandle) -> String {
|
|
81
|
-
match app.stop_remote_ui().await {
|
|
82
|
-
Ok(()) => format!("Server Stoped"),
|
|
83
|
-
Err(err) => format!("Server Error {:?}", err),
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
5. **Install the NPM plugin** in your frontend `npm i tauri-remote-ui`.
|
|
88
|
-
6. **Replace the NPM package**
|
|
89
|
-
```typescript
|
|
90
|
-
import { invoke } from "@tauri-apps/api/core";
|
|
91
|
-
import { listen } from "@tauri-apps/api/event";
|
|
92
|
-
```
|
|
93
|
-
To
|
|
94
|
-
```typescript
|
|
95
|
-
import { invoke } from "tauri-remote-ui/api/core";
|
|
96
|
-
import { listen } from "tauri-remote-ui/api/event";
|
|
97
|
-
```
|
|
98
|
-
7. **Development WebSocket Proxy** `/remote_ui_ws` proxy remote_ui url ws to your dev server like vite.
|
|
99
|
-
8. **Enable Source Map and update lauch.json setup in vscode to debug frontend**
|
|
100
|
-
|
|
101
|
-
## Plugin Development
|
|
102
|
-
|
|
103
|
-
- Build Rust: `cargo build`
|
|
104
|
-
- Build JS: `cd guest-js && pnpm build`
|
|
105
|
-
- Example app: See `examples/tauri-app/`
|
|
106
|
-
|
|
107
|
-
## License
|
|
108
|
-
|
|
109
|
-
MIT
|
|
65
|
+
Refer document central for detailed information [docs](https://docs.draviavemal.com)
|
package/api/core/index.cjs
CHANGED
|
@@ -8,52 +8,46 @@ var socket = require('../../socket.cjs');
|
|
|
8
8
|
*
|
|
9
9
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
10
10
|
*/
|
|
11
|
+
/** Monotonic request id, encapsulated in module scope. */
|
|
12
|
+
let nextRequestId = 0;
|
|
11
13
|
/**
|
|
12
|
-
* Invoke a command on the Tauri application
|
|
13
|
-
* Falls back to WebSocket if Tauri IPC is not available
|
|
14
|
+
* Invoke a command on the Tauri application.
|
|
15
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
14
16
|
*
|
|
15
17
|
* @param cmd - The command name to invoke
|
|
16
18
|
* @param args - Arguments to pass to the command
|
|
17
19
|
* @param options - Options for the command
|
|
18
20
|
*/
|
|
19
|
-
let msg_id = 0;
|
|
20
21
|
async function invoke(cmd, args, options) {
|
|
21
|
-
if ((
|
|
22
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
22
|
+
if (socket.hasTauriRuntime()) {
|
|
23
23
|
return await core.invoke(cmd, args, options);
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (socket.wsReady) {
|
|
29
|
-
await socket.wsReady;
|
|
30
|
-
}
|
|
31
|
-
if (socket.ws && socket.ws.readyState === WebSocket.OPEN) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const msg = {
|
|
34
|
-
id: ++msg_id,
|
|
35
|
-
cmd, args, options
|
|
36
|
-
};
|
|
37
|
-
let clear = setTimeout(() => {
|
|
38
|
-
delete socket.filterCollection[msg_id];
|
|
39
|
-
reject(`Invoke Timeout. cmd : ${cmd}`);
|
|
40
|
-
}, 30000);
|
|
41
|
-
socket.filterCollection[msg_id] = ({ status, payload }) => {
|
|
42
|
-
clearTimeout(clear);
|
|
43
|
-
if (status = "success") {
|
|
44
|
-
resolve(payload);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
reject(payload);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
socket.ws.send(JSON.stringify(msg));
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
55
|
-
}
|
|
25
|
+
socket.initWebSocket();
|
|
26
|
+
if (socket.wsReady) {
|
|
27
|
+
await socket.wsReady;
|
|
56
28
|
}
|
|
29
|
+
if (!socket.ws || socket.ws.readyState !== WebSocket.OPEN) {
|
|
30
|
+
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
31
|
+
}
|
|
32
|
+
const requestId = ++nextRequestId;
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
const message = { id: requestId, cmd, args, options };
|
|
35
|
+
const timeoutHandle = setTimeout(() => {
|
|
36
|
+
delete socket.filterCollection[requestId];
|
|
37
|
+
reject(new Error(`Invoke timeout. cmd: ${cmd}`));
|
|
38
|
+
}, 30000);
|
|
39
|
+
socket.filterCollection[requestId] = ({ status, payload }) => {
|
|
40
|
+
clearTimeout(timeoutHandle);
|
|
41
|
+
delete socket.filterCollection[requestId];
|
|
42
|
+
if (status === socket.RpcStatus.Success) {
|
|
43
|
+
resolve(payload);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
reject(payload);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
socket.ws.send(JSON.stringify(message));
|
|
50
|
+
});
|
|
57
51
|
}
|
|
58
52
|
|
|
59
53
|
exports.invoke = invoke;
|
package/api/core/index.d.ts
CHANGED
|
@@ -4,4 +4,12 @@
|
|
|
4
4
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
5
5
|
*/
|
|
6
6
|
import { InvokeArgs, InvokeOptions } from '@tauri-apps/api/core';
|
|
7
|
+
/**
|
|
8
|
+
* Invoke a command on the Tauri application.
|
|
9
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
10
|
+
*
|
|
11
|
+
* @param cmd - The command name to invoke
|
|
12
|
+
* @param args - Arguments to pass to the command
|
|
13
|
+
* @param options - Options for the command
|
|
14
|
+
*/
|
|
7
15
|
export declare function invoke<T>(cmd: string, args?: InvokeArgs, options?: InvokeOptions): Promise<T>;
|
package/api/core/index.js
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
1
|
import { invoke as invoke$1 } from '@tauri-apps/api/core';
|
|
2
|
-
import { initWebSocket, wsReady, ws, filterCollection } from '../../socket.js';
|
|
2
|
+
import { hasTauriRuntime, initWebSocket, wsReady, ws, filterCollection, RpcStatus } from '../../socket.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Core API module for Tauri Remote UI
|
|
6
6
|
*
|
|
7
7
|
* This module handles sending messages to the Tauri application via WebSocket
|
|
8
8
|
*/
|
|
9
|
+
/** Monotonic request id, encapsulated in module scope. */
|
|
10
|
+
let nextRequestId = 0;
|
|
9
11
|
/**
|
|
10
|
-
* Invoke a command on the Tauri application
|
|
11
|
-
* Falls back to WebSocket if Tauri IPC is not available
|
|
12
|
+
* Invoke a command on the Tauri application.
|
|
13
|
+
* Falls back to a WebSocket transport if Tauri IPC is not available.
|
|
12
14
|
*
|
|
13
15
|
* @param cmd - The command name to invoke
|
|
14
16
|
* @param args - Arguments to pass to the command
|
|
15
17
|
* @param options - Options for the command
|
|
16
18
|
*/
|
|
17
|
-
let msg_id = 0;
|
|
18
19
|
async function invoke(cmd, args, options) {
|
|
19
|
-
if ((
|
|
20
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
20
|
+
if (hasTauriRuntime()) {
|
|
21
21
|
return await invoke$1(cmd, args, options);
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (wsReady) {
|
|
27
|
-
await wsReady;
|
|
28
|
-
}
|
|
29
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
30
|
-
return new Promise((resolve, reject) => {
|
|
31
|
-
const msg = {
|
|
32
|
-
id: ++msg_id,
|
|
33
|
-
cmd, args, options
|
|
34
|
-
};
|
|
35
|
-
let clear = setTimeout(() => {
|
|
36
|
-
delete filterCollection[msg_id];
|
|
37
|
-
reject(`Invoke Timeout. cmd : ${cmd}`);
|
|
38
|
-
}, 30000);
|
|
39
|
-
filterCollection[msg_id] = ({ status, payload }) => {
|
|
40
|
-
clearTimeout(clear);
|
|
41
|
-
if (status = "success") {
|
|
42
|
-
resolve(payload);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
reject(payload);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
ws.send(JSON.stringify(msg));
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
else {
|
|
52
|
-
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
53
|
-
}
|
|
23
|
+
initWebSocket();
|
|
24
|
+
if (wsReady) {
|
|
25
|
+
await wsReady;
|
|
54
26
|
}
|
|
27
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
28
|
+
throw new Error('No WebSocket or Tauri IPC available to invoke');
|
|
29
|
+
}
|
|
30
|
+
const requestId = ++nextRequestId;
|
|
31
|
+
return await new Promise((resolve, reject) => {
|
|
32
|
+
const message = { id: requestId, cmd, args, options };
|
|
33
|
+
const timeoutHandle = setTimeout(() => {
|
|
34
|
+
delete filterCollection[requestId];
|
|
35
|
+
reject(new Error(`Invoke timeout. cmd: ${cmd}`));
|
|
36
|
+
}, 30000);
|
|
37
|
+
filterCollection[requestId] = ({ status, payload }) => {
|
|
38
|
+
clearTimeout(timeoutHandle);
|
|
39
|
+
delete filterCollection[requestId];
|
|
40
|
+
if (status === RpcStatus.Success) {
|
|
41
|
+
resolve(payload);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
reject(payload);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
ws.send(JSON.stringify(message));
|
|
48
|
+
});
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
export { invoke };
|
package/api/event/index.cjs
CHANGED
|
@@ -9,48 +9,32 @@ var socket = require('../../socket.cjs');
|
|
|
9
9
|
* This module handles listening to events from the Tauri application via WebSocket
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* Listen to events from the Tauri application
|
|
13
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
12
|
+
* Listen to events from the Tauri application.
|
|
13
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
14
14
|
*
|
|
15
15
|
* @param event - The event name to listen for
|
|
16
16
|
* @param handler - Callback to handle the event
|
|
17
17
|
* @param options - Options for the event listener
|
|
18
18
|
*/
|
|
19
19
|
async function listen(event$1, handler, options) {
|
|
20
|
-
if ((
|
|
21
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
20
|
+
if (socket.hasTauriRuntime()) {
|
|
22
21
|
return await event.listen(event$1, handler, options);
|
|
23
22
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (socket.wsReady) {
|
|
28
|
-
await socket.wsReady;
|
|
29
|
-
}
|
|
30
|
-
if (socket.ws && socket.ws.readyState === WebSocket.OPEN) {
|
|
31
|
-
// Handle WebSocket messages for events
|
|
32
|
-
const messageHandler = (wsEvent) => {
|
|
33
|
-
try {
|
|
34
|
-
const data = JSON.parse(wsEvent.data);
|
|
35
|
-
if (data.event === event$1) {
|
|
36
|
-
handler(data);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
catch (err) {
|
|
40
|
-
console.error(err);
|
|
41
|
-
throw new Error('Error handling WebSocket event');
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
socket.ws.addEventListener('message', messageHandler);
|
|
45
|
-
// Return an unlisten function
|
|
46
|
-
return () => {
|
|
47
|
-
socket.ws === null || socket.ws === void 0 ? void 0 : socket.ws.removeEventListener('message', messageHandler);
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
throw new Error("No WebSocket or Tauri IPC available to invoke");
|
|
52
|
-
}
|
|
23
|
+
socket.initWebSocket();
|
|
24
|
+
if (socket.wsReady) {
|
|
25
|
+
await socket.wsReady;
|
|
53
26
|
}
|
|
27
|
+
const messageHandler = (e) => {
|
|
28
|
+
handler(e.data);
|
|
29
|
+
};
|
|
30
|
+
socket.listenEvent.addEventListener(event$1, messageHandler);
|
|
31
|
+
return () => {
|
|
32
|
+
socket.listenEvent.removeEventListener(event$1, messageHandler);
|
|
33
|
+
};
|
|
54
34
|
}
|
|
55
35
|
|
|
36
|
+
Object.defineProperty(exports, "latencyMs", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
get: function () { return socket.latencyMs; }
|
|
39
|
+
});
|
|
56
40
|
exports.listen = listen;
|
package/api/event/index.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { EventCallback, EventName, Options, UnlistenFn } from '@tauri-apps/api/event';
|
|
7
7
|
export type { UnlistenFn } from '@tauri-apps/api/event';
|
|
8
|
+
export { latencyMs } from '../../socket';
|
|
8
9
|
/**
|
|
9
|
-
* Listen to events from the Tauri application
|
|
10
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
10
|
+
* Listen to events from the Tauri application.
|
|
11
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
11
12
|
*
|
|
12
13
|
* @param event - The event name to listen for
|
|
13
14
|
* @param handler - Callback to handle the event
|
package/api/event/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listen as listen$1 } from '@tauri-apps/api/event';
|
|
2
|
-
import { initWebSocket,
|
|
2
|
+
import { hasTauriRuntime, initWebSocket, listenEvent, wsReady } from '../../socket.js';
|
|
3
|
+
export { latencyMs } from '../../socket.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Event API module for Tauri Remote UI
|
|
@@ -7,48 +8,28 @@ import { initWebSocket, wsReady, ws } from '../../socket.js';
|
|
|
7
8
|
* This module handles listening to events from the Tauri application via WebSocket
|
|
8
9
|
*/
|
|
9
10
|
/**
|
|
10
|
-
* Listen to events from the Tauri application
|
|
11
|
-
* Falls back to WebSocket if Tauri Event API is not available
|
|
11
|
+
* Listen to events from the Tauri application.
|
|
12
|
+
* Falls back to a WebSocket transport if the Tauri Event API is not available.
|
|
12
13
|
*
|
|
13
14
|
* @param event - The event name to listen for
|
|
14
15
|
* @param handler - Callback to handle the event
|
|
15
16
|
* @param options - Options for the event listener
|
|
16
17
|
*/
|
|
17
18
|
async function listen(event, handler, options) {
|
|
18
|
-
if ((
|
|
19
|
-
window.__TAURI__ && window.__TAURI__.invoke) {
|
|
19
|
+
if (hasTauriRuntime()) {
|
|
20
20
|
return await listen$1(event, handler, options);
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (wsReady) {
|
|
26
|
-
await wsReady;
|
|
27
|
-
}
|
|
28
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
29
|
-
// Handle WebSocket messages for events
|
|
30
|
-
const messageHandler = (wsEvent) => {
|
|
31
|
-
try {
|
|
32
|
-
const data = JSON.parse(wsEvent.data);
|
|
33
|
-
if (data.event === event) {
|
|
34
|
-
handler(data);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
console.error(err);
|
|
39
|
-
throw new Error('Error handling WebSocket event');
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
ws.addEventListener('message', messageHandler);
|
|
43
|
-
// Return an unlisten function
|
|
44
|
-
return () => {
|
|
45
|
-
ws === null || ws === void 0 ? void 0 : ws.removeEventListener('message', messageHandler);
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
throw new Error("No WebSocket or Tauri IPC available to invoke");
|
|
50
|
-
}
|
|
22
|
+
initWebSocket();
|
|
23
|
+
if (wsReady) {
|
|
24
|
+
await wsReady;
|
|
51
25
|
}
|
|
26
|
+
const messageHandler = (e) => {
|
|
27
|
+
handler(e.data);
|
|
28
|
+
};
|
|
29
|
+
listenEvent.addEventListener(event, messageHandler);
|
|
30
|
+
return () => {
|
|
31
|
+
listenEvent.removeEventListener(event, messageHandler);
|
|
32
|
+
};
|
|
52
33
|
}
|
|
53
34
|
|
|
54
35
|
export { listen };
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tauri-remote-ui",
|
|
3
|
-
"license": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"license": "AGPL-3.0-only",
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"author": "DraviaVemal",
|
|
6
6
|
"description": "A Tauri plugin that exposes the application's UI to a web browser, allowing full interaction while the native app continues running. This enables end-to-end UI testing using existing web-based testing tools without requiring modifications to the app itself.",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"types": "./index.d.ts",
|
|
9
|
-
"main": "./index.cjs",
|
|
10
|
-
"module": "./index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"main": "./dist/index.cjs",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
11
|
"repository": {
|
|
12
12
|
"url": "https://github.com/DraviaVemal/tauri-remote-ui"
|
|
13
13
|
},
|
|
14
|
+
"readme": "./README.md",
|
|
14
15
|
"exports": {
|
|
15
16
|
".": {
|
|
16
17
|
"types": "./index.d.ts",
|
package/socket.cjs
CHANGED
|
@@ -1,78 +1,138 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var version = require('./version.cjs');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
|
-
* Tauri Remote UI -
|
|
6
|
+
* Tauri Remote UI - WebSocket bridge
|
|
5
7
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
9
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
10
|
+
* exported from `./api/core` and `./api/event`.
|
|
9
11
|
*/
|
|
12
|
+
/** Wire prefix used by the version-handshake exchange. */
|
|
13
|
+
const VERSION_PREFIX = 'version:';
|
|
14
|
+
/**
|
|
15
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
16
|
+
*
|
|
17
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` β keep both in sync.
|
|
18
|
+
*/
|
|
19
|
+
const RpcStatus = {
|
|
20
|
+
Success: 'success',
|
|
21
|
+
Error: 'error',
|
|
22
|
+
};
|
|
23
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
24
|
+
function hasTauriRuntime() {
|
|
25
|
+
const w = window;
|
|
26
|
+
return Boolean((w.__TAURI_INTERNALS__ && w.__TAURI_INTERNALS__.invoke) ||
|
|
27
|
+
(w.__TAURI__ && w.__TAURI__.invoke));
|
|
28
|
+
}
|
|
10
29
|
exports.ws = null;
|
|
30
|
+
const listenEvent = new EventTarget();
|
|
11
31
|
exports.wsReady = null;
|
|
12
32
|
const filterCollection = {};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
33
|
+
exports.latencyMs = 0;
|
|
34
|
+
/** Build the WebSocket URL for the RPC connection. */
|
|
16
35
|
function getWsUrl() {
|
|
17
36
|
const loc = window.location;
|
|
18
37
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
19
|
-
|
|
20
|
-
return wsUrl;
|
|
38
|
+
return `${proto}//${loc.host}/remote_ui_ws`;
|
|
21
39
|
}
|
|
22
|
-
|
|
40
|
+
/** Build the disconnect-redirect URL the page navigates to on close. */
|
|
41
|
+
function getDisconnectUrl() {
|
|
23
42
|
const loc = window.location;
|
|
24
|
-
|
|
25
|
-
return wsUrl;
|
|
43
|
+
return `${loc.protocol}//${loc.host}/remote_ui_disconnect`;
|
|
26
44
|
}
|
|
27
45
|
/**
|
|
28
|
-
* Initialize the WebSocket connection
|
|
29
|
-
*
|
|
46
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
47
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
30
48
|
*/
|
|
31
49
|
function initWebSocket() {
|
|
50
|
+
if (hasTauriRuntime()) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (exports.ws) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.info('Tauri-Remote-UI : Remote RPC Attempting...');
|
|
57
|
+
const wsUrl = getWsUrl();
|
|
32
58
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
59
|
+
let lastPingTimestamp = Date.now();
|
|
60
|
+
let pingPongTimer;
|
|
61
|
+
const socket = new WebSocket(wsUrl);
|
|
62
|
+
exports.ws = socket;
|
|
63
|
+
exports.wsReady = new Promise((resolve, reject) => {
|
|
64
|
+
socket.onopen = () => {
|
|
65
|
+
console.info('Tauri-Remote-UI : Remote Connected.');
|
|
66
|
+
socket.send(`${VERSION_PREFIX}${version.PACKAGE_VERSION}`);
|
|
67
|
+
lastPingTimestamp = Date.now();
|
|
68
|
+
socket.send('ping');
|
|
69
|
+
pingPongTimer = setInterval(() => {
|
|
70
|
+
lastPingTimestamp = Date.now();
|
|
71
|
+
socket.send('ping');
|
|
72
|
+
}, 10000);
|
|
73
|
+
resolve();
|
|
74
|
+
};
|
|
75
|
+
socket.onmessage = ({ data }) => {
|
|
76
|
+
var _a;
|
|
77
|
+
if (data === 'pong') {
|
|
78
|
+
exports.latencyMs = Date.now() - lastPingTimestamp;
|
|
79
|
+
if (exports.latencyMs > 200) {
|
|
80
|
+
console.warn(`Tauri-Remote-UI : High latency detected - ${exports.latencyMs}ms`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof data === 'string' && data.startsWith(VERSION_PREFIX)) {
|
|
85
|
+
const serverVersion = data.slice(VERSION_PREFIX.length);
|
|
86
|
+
if (serverVersion !== version.PACKAGE_VERSION) {
|
|
87
|
+
console.warn(`Tauri-Remote-UI : Version mismatch β frontend ` +
|
|
88
|
+
`'tauri-remote-ui' npm package is ${version.PACKAGE_VERSION}, ` +
|
|
89
|
+
`host crate is ${serverVersion}. Behavior is undefined; ` +
|
|
90
|
+
`align both to the same release.`);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let jsonData;
|
|
95
|
+
try {
|
|
96
|
+
jsonData = JSON.parse(data);
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.warn('Tauri-Remote-UI : Failed to parse message', err);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (typeof jsonData.id === 'number' && filterCollection[jsonData.id]) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse((_a = jsonData.payload) !== null && _a !== void 0 ? _a : 'null');
|
|
105
|
+
filterCollection[jsonData.id](parsed);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
console.warn('Tauri-Remote-UI : Failed to parse RPC payload', err);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (jsonData.event) {
|
|
112
|
+
listenEvent.dispatchEvent(new MessageEvent(jsonData.event, { data: jsonData }));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
socket.onclose = () => {
|
|
116
|
+
exports.ws = null;
|
|
117
|
+
exports.wsReady = null;
|
|
118
|
+
if (pingPongTimer !== undefined) {
|
|
119
|
+
clearInterval(pingPongTimer);
|
|
120
|
+
}
|
|
121
|
+
console.info('Tauri-Remote-UI : Remote Disconnected.');
|
|
122
|
+
window.location.href = getDisconnectUrl();
|
|
123
|
+
};
|
|
124
|
+
socket.onerror = (e) => {
|
|
125
|
+
reject(e);
|
|
126
|
+
};
|
|
127
|
+
});
|
|
41
128
|
}
|
|
42
|
-
catch {
|
|
43
|
-
|
|
44
|
-
return;
|
|
45
|
-
console.info("Remote RPC Attempting...");
|
|
46
|
-
const wsUrl = getWsUrl();
|
|
47
|
-
try {
|
|
48
|
-
exports.ws = new WebSocket(wsUrl);
|
|
49
|
-
exports.wsReady = new Promise((resolve, reject) => {
|
|
50
|
-
exports.ws.onopen = () => {
|
|
51
|
-
console.info("Remote Connected.");
|
|
52
|
-
resolve();
|
|
53
|
-
};
|
|
54
|
-
exports.ws.onclose = () => {
|
|
55
|
-
exports.ws = null;
|
|
56
|
-
exports.wsReady = null;
|
|
57
|
-
console.info("Remote Dis-Connected.");
|
|
58
|
-
window.location.replace(getUrl());
|
|
59
|
-
};
|
|
60
|
-
exports.ws.onerror = (e) => {
|
|
61
|
-
reject(e);
|
|
62
|
-
};
|
|
63
|
-
exports.ws.onmessage = ({ data }) => {
|
|
64
|
-
let json_data = JSON.parse(data);
|
|
65
|
-
json_data.id && filterCollection[json_data.id] && filterCollection[json_data.id](JSON.parse(json_data.payload));
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
catch (e) {
|
|
70
|
-
setTimeout(() => {
|
|
71
|
-
initWebSocket();
|
|
72
|
-
}, 5000);
|
|
73
|
-
}
|
|
129
|
+
catch (e) {
|
|
130
|
+
console.error(e);
|
|
74
131
|
}
|
|
75
132
|
}
|
|
76
133
|
|
|
134
|
+
exports.RpcStatus = RpcStatus;
|
|
77
135
|
exports.filterCollection = filterCollection;
|
|
136
|
+
exports.hasTauriRuntime = hasTauriRuntime;
|
|
78
137
|
exports.initWebSocket = initWebSocket;
|
|
138
|
+
exports.listenEvent = listenEvent;
|
package/socket.d.ts
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tauri Remote UI -
|
|
2
|
+
* Tauri Remote UI - WebSocket bridge
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
5
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
6
|
+
* exported from `./api/core` and `./api/event`.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` β keep both in sync.
|
|
12
|
+
*/
|
|
13
|
+
export declare const RpcStatus: {
|
|
14
|
+
readonly Success: "success";
|
|
15
|
+
readonly Error: "error";
|
|
16
|
+
};
|
|
17
|
+
export type RpcStatus = typeof RpcStatus[keyof typeof RpcStatus];
|
|
18
|
+
/** Shape of the response payload returned for a single RPC call. */
|
|
19
|
+
export interface RpcResponse<T = unknown> {
|
|
20
|
+
status: RpcStatus;
|
|
21
|
+
payload: T;
|
|
22
|
+
}
|
|
23
|
+
/** Callback type stored per outstanding RPC request id. */
|
|
24
|
+
export type RpcResponseHandler = (response: RpcResponse) => void;
|
|
25
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
26
|
+
export declare function hasTauriRuntime(): boolean;
|
|
8
27
|
export declare let ws: WebSocket | null;
|
|
28
|
+
export declare const listenEvent: EventTarget;
|
|
9
29
|
export declare let wsReady: Promise<void> | null;
|
|
10
|
-
export declare const filterCollection:
|
|
11
|
-
|
|
12
|
-
};
|
|
30
|
+
export declare const filterCollection: Record<number, RpcResponseHandler>;
|
|
31
|
+
export declare let latencyMs: number;
|
|
13
32
|
/**
|
|
14
|
-
* Initialize the WebSocket connection
|
|
15
|
-
*
|
|
33
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
34
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
16
35
|
*/
|
|
17
36
|
export declare function initWebSocket(): void;
|
package/socket.js
CHANGED
|
@@ -1,75 +1,132 @@
|
|
|
1
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
* Tauri Remote UI -
|
|
4
|
+
* Tauri Remote UI - WebSocket bridge
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* Establishes (and re-uses) a WebSocket connection back to the Tauri host
|
|
7
|
+
* application, providing the transport for the `invoke` and `listen` shims
|
|
8
|
+
* exported from `./api/core` and `./api/event`.
|
|
7
9
|
*/
|
|
10
|
+
/** Wire prefix used by the version-handshake exchange. */
|
|
11
|
+
const VERSION_PREFIX = 'version:';
|
|
12
|
+
/**
|
|
13
|
+
* Status discriminator on the response payload sent back from the Rust side.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the `RpcStatus` enum in `src/models.rs` β keep both in sync.
|
|
16
|
+
*/
|
|
17
|
+
const RpcStatus = {
|
|
18
|
+
Success: 'success',
|
|
19
|
+
Error: 'error',
|
|
20
|
+
};
|
|
21
|
+
/** Returns true if the page is running inside a Tauri webview. */
|
|
22
|
+
function hasTauriRuntime() {
|
|
23
|
+
const w = window;
|
|
24
|
+
return Boolean((w.__TAURI_INTERNALS__ && w.__TAURI_INTERNALS__.invoke) ||
|
|
25
|
+
(w.__TAURI__ && w.__TAURI__.invoke));
|
|
26
|
+
}
|
|
8
27
|
let ws = null;
|
|
28
|
+
const listenEvent = new EventTarget();
|
|
9
29
|
let wsReady = null;
|
|
10
30
|
const filterCollection = {};
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*/
|
|
31
|
+
let latencyMs = 0;
|
|
32
|
+
/** Build the WebSocket URL for the RPC connection. */
|
|
14
33
|
function getWsUrl() {
|
|
15
34
|
const loc = window.location;
|
|
16
35
|
const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
17
|
-
|
|
18
|
-
return wsUrl;
|
|
36
|
+
return `${proto}//${loc.host}/remote_ui_ws`;
|
|
19
37
|
}
|
|
20
|
-
|
|
38
|
+
/** Build the disconnect-redirect URL the page navigates to on close. */
|
|
39
|
+
function getDisconnectUrl() {
|
|
21
40
|
const loc = window.location;
|
|
22
|
-
|
|
23
|
-
return wsUrl;
|
|
41
|
+
return `${loc.protocol}//${loc.host}/remote_ui_disconnect`;
|
|
24
42
|
}
|
|
25
43
|
/**
|
|
26
|
-
* Initialize the WebSocket connection
|
|
27
|
-
*
|
|
44
|
+
* Initialize the WebSocket connection on first use. A no-op when running
|
|
45
|
+
* inside Tauri (native IPC is preferred) or when the socket is already open.
|
|
28
46
|
*/
|
|
29
47
|
function initWebSocket() {
|
|
48
|
+
if (hasTauriRuntime()) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (ws) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.info('Tauri-Remote-UI : Remote RPC Attempting...');
|
|
55
|
+
const wsUrl = getWsUrl();
|
|
30
56
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
57
|
+
let lastPingTimestamp = Date.now();
|
|
58
|
+
let pingPongTimer;
|
|
59
|
+
const socket = new WebSocket(wsUrl);
|
|
60
|
+
ws = socket;
|
|
61
|
+
wsReady = new Promise((resolve, reject) => {
|
|
62
|
+
socket.onopen = () => {
|
|
63
|
+
console.info('Tauri-Remote-UI : Remote Connected.');
|
|
64
|
+
socket.send(`${VERSION_PREFIX}${PACKAGE_VERSION}`);
|
|
65
|
+
lastPingTimestamp = Date.now();
|
|
66
|
+
socket.send('ping');
|
|
67
|
+
pingPongTimer = setInterval(() => {
|
|
68
|
+
lastPingTimestamp = Date.now();
|
|
69
|
+
socket.send('ping');
|
|
70
|
+
}, 10000);
|
|
71
|
+
resolve();
|
|
72
|
+
};
|
|
73
|
+
socket.onmessage = ({ data }) => {
|
|
74
|
+
var _a;
|
|
75
|
+
if (data === 'pong') {
|
|
76
|
+
latencyMs = Date.now() - lastPingTimestamp;
|
|
77
|
+
if (latencyMs > 200) {
|
|
78
|
+
console.warn(`Tauri-Remote-UI : High latency detected - ${latencyMs}ms`);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (typeof data === 'string' && data.startsWith(VERSION_PREFIX)) {
|
|
83
|
+
const serverVersion = data.slice(VERSION_PREFIX.length);
|
|
84
|
+
if (serverVersion !== PACKAGE_VERSION) {
|
|
85
|
+
console.warn(`Tauri-Remote-UI : Version mismatch β frontend ` +
|
|
86
|
+
`'tauri-remote-ui' npm package is ${PACKAGE_VERSION}, ` +
|
|
87
|
+
`host crate is ${serverVersion}. Behavior is undefined; ` +
|
|
88
|
+
`align both to the same release.`);
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
let jsonData;
|
|
93
|
+
try {
|
|
94
|
+
jsonData = JSON.parse(data);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.warn('Tauri-Remote-UI : Failed to parse message', err);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (typeof jsonData.id === 'number' && filterCollection[jsonData.id]) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse((_a = jsonData.payload) !== null && _a !== void 0 ? _a : 'null');
|
|
103
|
+
filterCollection[jsonData.id](parsed);
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
console.warn('Tauri-Remote-UI : Failed to parse RPC payload', err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (jsonData.event) {
|
|
110
|
+
listenEvent.dispatchEvent(new MessageEvent(jsonData.event, { data: jsonData }));
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
socket.onclose = () => {
|
|
114
|
+
ws = null;
|
|
115
|
+
wsReady = null;
|
|
116
|
+
if (pingPongTimer !== undefined) {
|
|
117
|
+
clearInterval(pingPongTimer);
|
|
118
|
+
}
|
|
119
|
+
console.info('Tauri-Remote-UI : Remote Disconnected.');
|
|
120
|
+
window.location.href = getDisconnectUrl();
|
|
121
|
+
};
|
|
122
|
+
socket.onerror = (e) => {
|
|
123
|
+
reject(e);
|
|
124
|
+
};
|
|
125
|
+
});
|
|
39
126
|
}
|
|
40
|
-
catch {
|
|
41
|
-
|
|
42
|
-
return;
|
|
43
|
-
console.info("Remote RPC Attempting...");
|
|
44
|
-
const wsUrl = getWsUrl();
|
|
45
|
-
try {
|
|
46
|
-
ws = new WebSocket(wsUrl);
|
|
47
|
-
wsReady = new Promise((resolve, reject) => {
|
|
48
|
-
ws.onopen = () => {
|
|
49
|
-
console.info("Remote Connected.");
|
|
50
|
-
resolve();
|
|
51
|
-
};
|
|
52
|
-
ws.onclose = () => {
|
|
53
|
-
ws = null;
|
|
54
|
-
wsReady = null;
|
|
55
|
-
console.info("Remote Dis-Connected.");
|
|
56
|
-
window.location.replace(getUrl());
|
|
57
|
-
};
|
|
58
|
-
ws.onerror = (e) => {
|
|
59
|
-
reject(e);
|
|
60
|
-
};
|
|
61
|
-
ws.onmessage = ({ data }) => {
|
|
62
|
-
let json_data = JSON.parse(data);
|
|
63
|
-
json_data.id && filterCollection[json_data.id] && filterCollection[json_data.id](JSON.parse(json_data.payload));
|
|
64
|
-
};
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
catch (e) {
|
|
68
|
-
setTimeout(() => {
|
|
69
|
-
initWebSocket();
|
|
70
|
-
}, 5000);
|
|
71
|
-
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
console.error(e);
|
|
72
129
|
}
|
|
73
130
|
}
|
|
74
131
|
|
|
75
|
-
export { filterCollection, initWebSocket, ws, wsReady };
|
|
132
|
+
export { RpcStatus, filterCollection, hasTauriRuntime, initWebSocket, latencyMs, listenEvent, ws, wsReady };
|
package/version.cjs
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
5
|
+
* handshake to detect frontend/backend version skew.
|
|
6
|
+
*
|
|
7
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
8
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
9
|
+
* value from the latest git tag.
|
|
10
|
+
*/
|
|
11
|
+
const PACKAGE_VERSION = '1.0.1';
|
|
12
|
+
|
|
13
|
+
exports.PACKAGE_VERSION = PACKAGE_VERSION;
|
package/version.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
3
|
+
* handshake to detect frontend/backend version skew.
|
|
4
|
+
*
|
|
5
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
6
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
7
|
+
* value from the latest git tag.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PACKAGE_VERSION = "1.0.1";
|
package/version.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-of-truth for the npm package's version, used in the WebSocket
|
|
3
|
+
* handshake to detect frontend/backend version skew.
|
|
4
|
+
*
|
|
5
|
+
* **Keep this in sync with `package.json` `version` and the Rust crate's
|
|
6
|
+
* `Cargo.toml` `version`.** The release flow (`DEVOPS_BUILD=1`) rewrites this
|
|
7
|
+
* value from the latest git tag.
|
|
8
|
+
*/
|
|
9
|
+
const PACKAGE_VERSION = '1.0.1';
|
|
10
|
+
|
|
11
|
+
export { PACKAGE_VERSION };
|