diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c42f98..ab574d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,13 +53,19 @@ jobs: node-version: '20' - name: Install dependencies working-directory: packages/tauri-plugin-conduit - run: npm install + run: npm ci - name: Type check working-directory: packages/tauri-plugin-conduit run: npx tsc --noEmit - name: Run TypeScript tests - run: npx tsx --test src/__tests__/*.test.ts working-directory: packages/tauri-plugin-conduit + run: npm test + - name: Smoke test npm package contents + working-directory: packages/tauri-plugin-conduit + run: | + npm run build + npm pack --dry-run 2>&1 | tee /tmp/tauri-plugin-conduit-pack.txt + grep 'dist/index.js' /tmp/tauri-plugin-conduit-pack.txt tauri-plugin-conduit: name: tauri-plugin-conduit diff --git a/README.md b/README.md index e6cda45..e155f22 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) [![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org) -**Drop-in replacement for Tauri's invoke(). One import change, zero config. Binary IPC under the hood.** +**Optional IPC path for Tauri apps that want a fetch-based transport with the same process-local runtime model. One import change, zero config, binary support when you need it.** ```diff - import { invoke } from '@tauri-apps/api/core'; @@ -25,6 +25,8 @@ ## Performance +Tauri's built-in `invoke()` is a solid default and a good fit for many apps. conduit is aimed at cases where transport overhead, built-in binary handling, or high-rate streaming become meaningful parts of the profile. + All numbers are **Rust dispatch layer only** (excludes WebView bridge, `fetch()`, JS parsing). See [BENCHMARKS.md](BENCHMARKS.md) for full methodology. Tauri invoke vs conduit — roundtrip latency by payload size @@ -35,19 +37,19 @@ All numbers are **Rust dispatch layer only** (excludes WebView bridge, `fetch()` | ~1 KB | 8.1 µs | 7.6 µs (**1.1x**) | 1.0 µs (**8x**) | | 64 KB | 2.27 ms | 834 µs (**2.7x**) | 202 µs (**11x**) | -### Why L1 barely wins at 1 KB +### Why L1 only changes the picture slightly at 1 KB Where does the time go? — Tauri invoke latency breakdown -At small payloads (25B), WebView transport overhead is 54% of total cost — conduit eliminates that and wins 2.2x. At 1 KB, JSON serialization is 82% of cost — transport is only 6%, so L1 (which still uses JSON) barely wins. L2 binary skips JSON entirely and delivers 8-11x regardless of payload size. +At small payloads (25B), WebView transport overhead is 54% of total cost, so the custom protocol path helps more. At 1 KB, JSON serialization is 82% of cost and transport is only 6%, so L1 stays closer to Tauri's built-in IPC. L2 binary skips JSON entirely and therefore changes the tradeoff more substantially. > Measured with [criterion](https://bheisler.github.io/criterion.rs/) on Intel i7-10700KF @ 3.80 GHz. Run `cd crates/conduit-core && cargo bench -- comparison` to see numbers on your hardware. ### Two levels of optimization -**Level 1 (drop-in)** — `invoke()` is API-compatible with Tauri's built-in invoke. Still uses JSON, but routes through conduit's in-process custom protocol and uses [sonic-rs](https://github.com/cloudwego/sonic-rs) (SIMD-accelerated) to deserialize directly to the target type in one step, skipping serde_json's intermediate `Value` conversion. +**Level 1 (drop-in)** — `invoke()` is API-compatible with Tauri's built-in invoke. It still uses JSON, but routes through conduit's in-process custom protocol and uses [sonic-rs](https://github.com/cloudwego/sonic-rs) (SIMD-accelerated) to deserialize directly to the target type in one step, skipping serde_json's intermediate `Value` conversion. -**Level 2 (binary)** — `invokeBinary()` eliminates JSON entirely. Raw bytes in, raw bytes out. Use `#[derive(Encode, Decode)]` for typed binary structs, or pass raw `Uint8Array` for full control. +**Level 2 (binary)** — `invokeBinary()` avoids JSON entirely. Raw bytes in, raw bytes out. Use `#[derive(Encode, Decode)]` for typed binary structs, or pass raw `Uint8Array` for full control. ## Getting Started @@ -132,7 +134,7 @@ Like Tauri's `#[tauri::command]`, tauri-conduit's `#[command]` macro automatical conduit includes built-in streaming from Rust to JavaScript via ring buffers and Tauri events. -> **Note:** The default `.channel("name")` creates a **lossy** ring buffer -- oldest frames are silently dropped when the buffer is full. Use `.channel_ordered("name")` for guaranteed-delivery ordered channels (backed by an unbounded queue -- monitor memory usage). +> **Note:** The default `.channel("name")` creates a **lossy** ring buffer -- oldest frames are silently dropped when the buffer is full. Use `.channel_ordered("name")` for guaranteed-delivery ordered channels. Both channel types default to a 64 KB budget; use `channel_ordered_with_capacity(0)` only if you explicitly want an unbounded ordered queue. Two channel types are available: @@ -238,7 +240,7 @@ sequenceDiagram CP->>JS: ArrayBuffer ``` -**Why Level 1 is faster even though it still uses JSON:** Tauri's built-in invoke deserializes JSON into an intermediate `serde_json::Value`, then converts that Value into your typed struct -- two deserialization steps. conduit uses [sonic-rs](https://github.com/cloudwego/sonic-rs) (SIMD-accelerated JSON) to deserialize directly from bytes to the target struct in one step, and routes through an in-process custom protocol instead of the webview message bridge. +**Why Level 1 can still help even though it still uses JSON:** Tauri's built-in invoke deserializes JSON into an intermediate `serde_json::Value`, then converts that value into your typed struct. conduit uses [sonic-rs](https://github.com/cloudwego/sonic-rs) (SIMD-accelerated JSON) to deserialize directly from bytes to the target struct in one step, and routes through an in-process custom protocol instead of the webview message bridge. | | Tauri `invoke()` | conduit `invoke()` | conduit `invokeBinary()` | |---|---|---|---| @@ -277,9 +279,11 @@ Everything runs in-process -- no ports, no sockets, no network endpoints. **Threat model**: The invoke key protects against cross-origin requests (other tabs, browser extensions intercepting network requests). It does **not** protect against malicious JavaScript running in the same WebView context -- any JS with access to the page can obtain the key via `fetch()` interception or DevTools. This matches Tauri's own trust model: the WebView JS context is trusted. Disable DevTools in production builds. -## Differences from Tauri's built-in IPC +## How it fits alongside Tauri's built-in IPC + +If Tauri's built-in IPC already meets your needs, it remains the simplest default. conduit is meant for projects that want a compatible `invoke()` surface, built-in binary request/response support, or higher-throughput streaming primitives. -Level 1 is a drop-in replacement — change one import and you're done. `#[tauri_conduit::command]` has full parity with `#[tauri::command]`: named parameters (camelCase conversion included), `State`, `AppHandle`, `Window`/`Webview` injection, async, and `Result`. +`#[tauri_conduit::command]` is designed to stay close to `#[tauri::command]`: named parameters (camelCase conversion included), `State`, `AppHandle`, `Window`/`Webview` injection, async, and `Result`. For streaming, conduit provides high-throughput ring buffer channels (`subscribe()`/`drain()`). For per-invocation progress callbacks, use `AppHandle::emit()` directly — handlers have full access to Tauri's event system via `AppHandle` injection. @@ -303,7 +307,7 @@ The `tauri-conduit` facade crate (6 lines) exists solely to enable the `#[tauri_ ```sh cargo test --workspace # core + derive crates cargo test --manifest-path crates/tauri-plugin-conduit/Cargo.toml # plugin unit tests -cd packages/tauri-plugin-conduit && npx tsx --test src/__tests__/*.test.ts # TS codec tests +cd packages/tauri-plugin-conduit && npm test # TS codec tests ``` > **Note:** There are no end-to-end integration tests that exercise the full Tauri->conduit->WebView roundtrip. The test suite covers unit-level Rust dispatch, codec correctness, and TypeScript wire format -- not the custom protocol transport under a running Tauri app. diff --git a/crates/tauri-plugin-conduit/README.md b/crates/tauri-plugin-conduit/README.md index 675696e..52c1122 100644 --- a/crates/tauri-plugin-conduit/README.md +++ b/crates/tauri-plugin-conduit/README.md @@ -12,7 +12,7 @@ Part of the [tauri-conduit](https://github.com/userFRM/tauri-conduit) workspace ## Usage ```rust -use conduit::{command, handler}; +use tauri_conduit::{command, handler}; #[command] fn greet(name: String) -> String { diff --git a/packages/tauri-plugin-conduit/.npmignore b/packages/tauri-plugin-conduit/.npmignore new file mode 100644 index 0000000..b7287c3 --- /dev/null +++ b/packages/tauri-plugin-conduit/.npmignore @@ -0,0 +1,2 @@ +src/__tests__/ +*.tsbuildinfo diff --git a/packages/tauri-plugin-conduit/README.md b/packages/tauri-plugin-conduit/README.md index f6131ce..dfb3cba 100644 --- a/packages/tauri-plugin-conduit/README.md +++ b/packages/tauri-plugin-conduit/README.md @@ -3,7 +3,7 @@ [![npm](https://img.shields.io/npm/v/tauri-plugin-conduit.svg)](https://www.npmjs.com/package/tauri-plugin-conduit) [![npm downloads](https://img.shields.io/npm/dm/tauri-plugin-conduit.svg)](https://www.npmjs.com/package/tauri-plugin-conduit) -Drop-in replacement for Tauri's `invoke()`. One import change, zero config. Binary IPC under the hood. +Optional IPC client for Tauri apps that want a fetch-based transport with binary support and a minimal API change. See the [main repository](https://github.com/userFRM/tauri-conduit) for full documentation, benchmarks, and architecture. @@ -15,7 +15,7 @@ npm install tauri-plugin-conduit ## Quick Start -Drop-in replacement for `@tauri-apps/api/core`: +Compatible `invoke()` surface: ```typescript import { invoke } from 'tauri-plugin-conduit'; @@ -44,7 +44,7 @@ const unsub = await subscribe('telemetry', (buf) => { ## API -- `invoke(cmd, args?, options?)` — JSON request/response (drop-in replacement for Tauri's `invoke`) +- `invoke(cmd, args?, options?)` — JSON request/response with a Tauri-compatible `invoke()` shape - `invokeBinary(cmd, payload?, options?)` — binary request/response (raw bytes) - `subscribe(channel, callback, onError?)` — event-driven push streaming (no polling) - `drain(channel)` — pull-based ring buffer access (user controls timing) diff --git a/packages/tauri-plugin-conduit/package-lock.json b/packages/tauri-plugin-conduit/package-lock.json index c83427f..169668d 100644 --- a/packages/tauri-plugin-conduit/package-lock.json +++ b/packages/tauri-plugin-conduit/package-lock.json @@ -1,18 +1,22 @@ { "name": "tauri-plugin-conduit", - "version": "1.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tauri-plugin-conduit", - "version": "1.0.0", + "version": "2.1.0", "license": "MIT OR Apache-2.0", "devDependencies": { "@tauri-apps/api": "^2.0.0", "@types/node": "^25.3.5", + "tsx": "^4.20.5", "typescript": "^5.7" }, + "engines": { + "node": ">=18" + }, "peerDependencies": { "@tauri-apps/api": "^2.0.0" }, @@ -22,6 +26,448 @@ } } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@tauri-apps/api": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", @@ -43,6 +489,106 @@ "undici-types": "~7.18.0" } }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/packages/tauri-plugin-conduit/package.json b/packages/tauri-plugin-conduit/package.json index f1bc9b7..d7fc9b7 100644 --- a/packages/tauri-plugin-conduit/package.json +++ b/packages/tauri-plugin-conduit/package.json @@ -20,7 +20,7 @@ "build": "tsc", "check": "tsc --noEmit", "prepublishOnly": "npm run build", - "test": "node --test src/__tests__/*.test.ts" + "test": "tsx --test src/__tests__/*.test.ts" }, "keywords": [ "tauri", @@ -41,6 +41,7 @@ "devDependencies": { "@tauri-apps/api": "^2.0.0", "@types/node": "^25.3.5", + "tsx": "^4.20.5", "typescript": "^5.7" }, "engines": { diff --git a/packages/tauri-plugin-conduit/src/index.ts b/packages/tauri-plugin-conduit/src/index.ts index f1dbe23..ff90499 100644 --- a/packages/tauri-plugin-conduit/src/index.ts +++ b/packages/tauri-plugin-conduit/src/index.ts @@ -36,7 +36,7 @@ */ import { listen, type UnlistenFn } from '@tauri-apps/api/event'; -import { bootstrap, type BootstrapInfo } from './negotiate.js'; +import { bootstrap, type BootstrapInfo, validateChannel } from './negotiate.js'; import { createProtocolTransport, type ProtocolTransport } from './transport/protocol.js'; import { ConduitError } from './error.js'; @@ -163,10 +163,11 @@ function buildConduit( callback: (data: ArrayBuffer) => void, onError?: (err: Error) => void, ): Promise { + await validateChannel(channel); + const unlisten = await listen( - 'conduit:data-available', - async (event) => { - if (event.payload !== channel) return; + `conduit:data-available:${channel}`, + async (_event) => { try { const buf = await drainChannel(channel); if (buf.byteLength > 0) { @@ -193,8 +194,12 @@ function buildConduit( if (buf.byteLength > 0) { callback(buf); } - } catch { - // Ignore — channel may be empty + } catch (err) { + if (onError) { + onError(err instanceof Error ? err : new Error(String(err))); + } else { + console.error(`conduit: initial drain error on channel "${channel}":`, err); + } } return wrappedUnlisten; }