use std::collections::HashMap;
use std::sync::Arc;
use allframe_core::router::Router;
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
use tauri::{Emitter, Manager, Runtime};
use tokio::sync::Mutex;
use crate::error::TauriServerError;
use crate::server::TauriServer;
use crate::types::{CallResponse, HandlerInfo, StreamStartResponse};
pub const PLUGIN_NAME: &str = "allframe-tauri";
pub(crate) fn stream_event(handler: &str, stream_id: &str) -> String {
format!("{PLUGIN_NAME}:stream:{handler}:{stream_id}")
}
pub(crate) fn boot_progress_event() -> String {
format!("{PLUGIN_NAME}:boot-progress")
}
pub(crate) struct ActiveStreams {
handles: Mutex<HashMap<String, tokio::task::AbortHandle>>,
}
impl ActiveStreams {
pub(crate) fn new() -> Self {
Self {
handles: Mutex::new(HashMap::new()),
}
}
}
#[tauri::command]
pub(crate) async fn allframe_list(
server: tauri::State<'_, TauriServer>,
) -> Result<Vec<HandlerInfo>, TauriServerError> {
Ok(server.list_handlers().to_vec())
}
#[tauri::command]
pub(crate) async fn allframe_call(
handler: String,
args: serde_json::Value,
server: tauri::State<'_, TauriServer>,
) -> Result<CallResponse, TauriServerError> {
let args_str = args.to_string();
server.call_handler(&handler, &args_str).await
}
#[tauri::command]
pub(crate) async fn allframe_stream<R: Runtime>(
handler: String,
args: serde_json::Value,
app: tauri::AppHandle<R>,
server: tauri::State<'_, TauriServer>,
active: tauri::State<'_, Arc<ActiveStreams>>,
) -> Result<StreamStartResponse, TauriServerError> {
let stream_id = uuid::Uuid::new_v4().to_string();
let args_str = args.to_string();
let (mut rx, join_handle) = server.call_streaming_handler(&handler, &args_str)?;
let sid = stream_id.clone();
let handler_name = handler.clone();
let app_clone = app.clone();
let active_inner: Arc<ActiveStreams> = (*active).clone();
let task = tokio::spawn(async move {
let event_base = stream_event(&handler_name, &sid);
while let Some(item) = rx.recv().await {
let _ = app_clone.emit(&event_base, &item);
}
match join_handle.await {
Ok(Ok(response)) => {
let _ = app_clone.emit(&format!("{event_base}:complete"), &response.result);
}
Ok(Err(e)) => {
let _ = app_clone.emit(&format!("{event_base}:error"), &e.to_string());
}
Err(e) => {
let _ = app_clone.emit(
&format!("{event_base}:error"),
&format!("Handler task panicked: {e}"),
);
}
}
active_inner.handles.lock().await.remove(&sid);
});
active
.handles
.lock()
.await
.insert(stream_id.clone(), task.abort_handle());
Ok(StreamStartResponse { stream_id })
}
#[tauri::command]
pub(crate) async fn allframe_stream_cancel<R: Runtime>(
stream_id: String,
app: tauri::AppHandle<R>,
active: tauri::State<'_, Arc<ActiveStreams>>,
) -> Result<(), TauriServerError> {
let mut handles = active.handles.lock().await;
match handles.remove(&stream_id) {
Some(abort_handle) => {
abort_handle.abort();
let _ = app.emit(
&format!("{}:cancelled", stream_event("unknown", &stream_id)),
&(),
);
Ok(())
}
None => Err(TauriServerError::ExecutionFailed(format!(
"Stream not found or already completed: {stream_id}"
))),
}
}
pub fn init<R: Runtime>(router: Router) -> TauriPlugin<R> {
build_plugin(move |app| {
let mut router = router;
router.inject_state(app.app_handle().clone());
app.manage(TauriServer::new(router));
app.manage(Arc::new(ActiveStreams::new()));
Ok(())
})
}
pub(crate) fn build_plugin<R, F>(setup: F) -> TauriPlugin<R>
where
R: Runtime,
F: FnOnce(&tauri::AppHandle<R>) -> Result<(), Box<dyn std::error::Error>> + Send + 'static,
{
PluginBuilder::new(PLUGIN_NAME)
.invoke_handler(tauri::generate_handler![
allframe_list,
allframe_call,
allframe_stream,
allframe_stream_cancel,
])
.setup(move |app, _api| {
setup(app.app_handle())?;
Ok(())
})
.build()
}
pub fn init_with_state<R: Runtime, S: Send + Sync + 'static>(
router: Router,
state: S,
) -> TauriPlugin<R> {
let router = router.with_state(state);
init(router)
}
pub fn builder<R: Runtime>(router: Router) -> crate::boot::BootBuilder<R> {
crate::boot::BootBuilder::new(router)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plugin_name_matches_crate_name() {
assert_eq!(PLUGIN_NAME, "allframe-tauri");
}
#[test]
fn plugin_name_contains_hyphen() {
assert!(
PLUGIN_NAME.contains('-'),
"Plugin name must include hyphen to match crate-derived ACL identifier"
);
}
#[test]
fn stream_event_uses_plugin_name_prefix() {
let event = stream_event("my_handler", "abc-123");
assert_eq!(event, "allframe-tauri:stream:my_handler:abc-123");
}
#[test]
fn stream_event_complete_suffix() {
let base = stream_event("handler", "id1");
let complete = format!("{base}:complete");
assert_eq!(complete, "allframe-tauri:stream:handler:id1:complete");
}
#[test]
fn stream_event_error_suffix() {
let base = stream_event("handler", "id1");
let error = format!("{base}:error");
assert_eq!(error, "allframe-tauri:stream:handler:id1:error");
}
#[test]
fn boot_progress_event_uses_plugin_name() {
assert_eq!(boot_progress_event(), "allframe-tauri:boot-progress");
}
#[test]
fn stream_event_does_not_use_old_prefix() {
let event = stream_event("handler", "id");
assert!(
!event.starts_with("allframe:"),
"Event must not use the old 'allframe:' prefix (should be 'allframe-tauri:')"
);
}
#[test]
fn boot_progress_event_does_not_use_old_prefix() {
let event = boot_progress_event();
assert!(
!event.starts_with("allframe:"),
"Boot progress event must not use the old 'allframe:' prefix"
);
}
}