Skip to main content

tauri_plugin_picoframe_worker/
lib.rs

1//! Example long-lived sidecar plugin (Rust half). Demonstrates the picoframe sidecar
2//! pattern: a persistent Bun HTTP server spawned once at startup, called over loopback
3//! with a shared-secret token, streaming progress to the UI as Tauri events.
4//!
5//! The heavy lifting (spawn, handshake, health, SSE bridge, restart, kill-on-exit) lives in
6//! [`picoframe_core::sidecar`]; this crate just configures it and exposes two commands.
7
8use picoframe_core::{CliResult, Sidecar, SidecarOptions};
9use serde_json::json;
10use std::time::Duration;
11use tauri::{
12    plugin::{Builder, TauriPlugin},
13    Manager, RunEvent, Runtime, State,
14};
15
16/// Report whether the sidecar server is up and answering `/health`.
17#[tauri::command]
18async fn worker_status(sidecar: State<'_, Sidecar>) -> Result<CliResult, String> {
19    let sidecar = sidecar.inner().clone();
20    let healthy = tauri::async_runtime::spawn_blocking(move || sidecar.healthy())
21        .await
22        .map_err(|e| e.to_string())?;
23    Ok(if healthy {
24        CliResult::ok(json!({ "healthy": true }))
25    } else {
26        CliResult::err("sidecar is not healthy")
27    })
28}
29
30/// Kick off a "crunch" job on the sidecar. Returns the final summary once done; per-item
31/// progress streams to the UI meanwhile as `picoframe://sidecar/worker/progress` events.
32#[tauri::command]
33async fn worker_crunch(count: u32, sidecar: State<'_, Sidecar>) -> Result<CliResult, String> {
34    let sidecar = sidecar.inner().clone();
35    let result = tauri::async_runtime::spawn_blocking(move || {
36        sidecar.request("crunch", json!({ "count": count }))
37    })
38    .await
39    .map_err(|e| e.to_string())?;
40    Ok(result)
41}
42
43/// Under `tauri dev` the compiled sidecar binary may not exist yet; fall back to running the
44/// plugin's `server.ts` with Bun. The path is resolved at compile time relative to this crate.
45fn dev_command() -> Option<Vec<String>> {
46    let server = concat!(env!("CARGO_MANIFEST_DIR"), "/../../plugins/worker/sidecar/server.ts");
47    std::path::Path::new(server)
48        .exists()
49        .then(|| vec!["bun".to_string(), "run".to_string(), server.to_string()])
50}
51
52/// Build the plugin. Registered as `"picoframe-worker"`, so the frontend invokes
53/// `plugin:picoframe-worker|worker_crunch`. The sidecar is spawned in `setup` and killed on
54/// app exit; a spawn failure is logged but does not abort startup (the UI surfaces the
55/// unhealthy state).
56pub fn init<R: Runtime>() -> TauriPlugin<R> {
57    Builder::new("picoframe-worker")
58        .invoke_handler(tauri::generate_handler![worker_status, worker_crunch])
59        .setup(|app, _api| {
60            let opts = SidecarOptions {
61                bin: "picoframe-worker-sidecar".to_string(),
62                dev_command: dev_command(),
63                event_prefix: "picoframe://sidecar/worker".to_string(),
64                ready_timeout: Duration::from_secs(15),
65                restart: true,
66            };
67            match Sidecar::spawn(app, opts) {
68                Ok(sidecar) => {
69                    app.manage(sidecar);
70                }
71                Err(e) => {
72                    eprintln!("[picoframe-worker] sidecar failed to start: {e}");
73                }
74            }
75            Ok(())
76        })
77        .on_event(|app, event| {
78            if matches!(event, RunEvent::Exit) {
79                if let Some(sidecar) = app.try_state::<Sidecar>() {
80                    sidecar.shutdown();
81                }
82            }
83        })
84        .build()
85}