use crate::{cancel_token::CancellationTokenListener, subscription::SubscriptionRequest};
mod builder;
pub use builder::{Builder, BuilderError};
use async_graphql::{
futures_util::StreamExt, BatchRequest, ObjectType, Request, Schema, SubscriptionType,
};
use serde::{de::IntoDeserializer, Deserialize};
use serde_json::Value as JsonValue;
use std::sync::Arc;
use tauri::{
ipc::{Invoke, InvokeError},
plugin::Plugin,
webview::PageLoadPayload,
AppHandle, Emitter, EventTarget, Manager, RunEvent, Runtime, Url, Webview, Window, WindowEvent,
};
pub(crate) type SetupHook<R, Q, M, S> = dyn FnOnce(&AppHandle<R>, JsonValue, &Schema<Q, M, S>) -> Result<(), Box<dyn std::error::Error>>
+ Send;
pub(crate) type OnWebviewReady<R> = dyn FnMut(Webview<R>) + Send;
pub(crate) type OnEvent<R> = dyn FnMut(&AppHandle<R>, &RunEvent) + Send;
pub(crate) type OnPageLoad<R> = dyn FnMut(&Webview<R>, &PageLoadPayload<'_>) + Send;
pub(crate) type OnDrop<R> = dyn FnOnce(AppHandle<R>) + Send;
pub(crate) type OnBatchRequest = dyn Fn(BatchRequest) -> BatchRequest + Send + Sync;
pub(crate) type OnSubRequst = dyn Fn(Request) -> Request + Send + Sync;
pub(crate) type OnWindowReady<R> = dyn FnMut(Window<R>) + Send;
pub(crate) type OnNavigation<R> = dyn Fn(&Webview<R>, &Url) -> bool + Send;
pub struct MizukiPlugin<R, Q, M, S>
where
R: Runtime,
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static,
{
name: &'static str,
app: Option<AppHandle<R>>,
schema: Schema<Q, M, S>,
setup: Option<Box<SetupHook<R, Q, M, S>>>,
js_init_script: Option<String>,
on_page_load: Box<OnPageLoad<R>>,
on_webview_ready: Box<OnWebviewReady<R>>,
on_event: Box<OnEvent<R>>,
on_drop: Option<Box<OnDrop<R>>>,
on_batch_request: Arc<Box<OnBatchRequest>>,
on_sub_request: Arc<Box<OnSubRequst>>,
on_window_ready: Box<OnWindowReady<R>>,
on_navigation: Box<OnNavigation<R>>,
auto_cancel: bool,
sub_end_event_label: String,
}
impl<R, Q, M, S> Drop for MizukiPlugin<R, Q, M, S>
where
R: Runtime,
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static,
{
fn drop(&mut self) {
if let (Some(on_drop), Some(app)) = (self.on_drop.take(), self.app.take()) {
on_drop(app);
}
}
}
impl<R, Q, M, S> Plugin<R> for MizukiPlugin<R, Q, M, S>
where
R: Runtime,
Q: ObjectType + 'static,
M: ObjectType + 'static,
S: SubscriptionType + 'static,
{
fn name(&self) -> &'static str {
self.name
}
fn initialize(
&mut self,
app: &AppHandle<R>,
config: JsonValue,
) -> Result<(), Box<dyn std::error::Error>> {
let _ = config;
self.app.replace(app.clone());
if let Some(s) = self.setup.take() {
(s)(app, config, &self.schema)?;
}
Ok(())
}
fn initialization_script(&self) -> Option<String> {
self.js_init_script.clone()
}
fn on_page_load(&mut self, window: &Webview<R>, payload: &PageLoadPayload<'_>) {
(self.on_page_load)(window, payload)
}
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {
(self.on_event)(app, event)
}
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
let on_batch_request = self.on_batch_request.clone();
let on_sub_request = self.on_sub_request.clone();
let sub_end_event_label = self.sub_end_event_label.clone();
let auto_cancel = self.auto_cancel;
let schema = self.schema.clone();
match invoke.message.command() {
"graphql" => invoke.resolver.respond_async(async move {
let req: BatchRequest = match invoke.message.payload() {
tauri::ipc::InvokeBody::Json(value) => {
serde_json::from_value(value.clone()).map_err(InvokeError::from_error)?
}
tauri::ipc::InvokeBody::Raw(vec) => {
Deserialize::deserialize(vec.clone().into_deserializer())
.map_err(|e: serde_json::Error| InvokeError::from_error(e))?
}
};
let resp = schema
.execute_batch((on_batch_request)(
req
.data(invoke.message.webview().app_handle().clone())
.data(invoke.message.webview())
.data(invoke.message.webview().window()),
))
.await;
let str = serde_json::to_string(&resp).map_err(InvokeError::from_error)?;
Ok((str, resp.is_ok()))
}),
"subscriptions" => invoke.resolver.respond_async(async move {
let window = invoke.message.webview();
let req: SubscriptionRequest = match invoke.message.payload() {
tauri::ipc::InvokeBody::Json(value) => {
serde_json::from_value(value.clone()).map_err(InvokeError::from_error)?
}
tauri::ipc::InvokeBody::Raw(vec) => {
Deserialize::deserialize(vec.clone().into_deserializer())
.map_err(|e: serde_json::Error| InvokeError::from_error(e))?
}
};
let subscription_webview = window.clone();
let webwiew_cancel_token = CancellationTokenListener::new(
subscription_webview.clone(),
sub_end_event_label,
req.sub_id.clone(),
);
let cancel_token = webwiew_cancel_token.token();
let mut stream = schema.execute_stream((on_sub_request)(
req
.inner
.data(invoke.message.webview().app_handle().clone())
.data(invoke.message.webview())
.data(invoke.message.webview().window())
.data(webwiew_cancel_token.token().clone()),
));
let _ca = {
let cancel_token = Arc::new(cancel_token.clone());
let weak_cancel_token = Arc::downgrade(&cancel_token);
subscription_webview.window().on_window_event(move |event| {
if let WindowEvent::Destroyed = event {
if let Some(token) = weak_cancel_token.upgrade() {
token.cancel();
}
}
});
cancel_token
};
let _d = cancel_token.drop_guard_ref();
let event_id = &format!("graphql://{}", req.id);
if auto_cancel {
loop {
tokio::select! {
_ = cancel_token.cancelled() => {
break;
},
res = stream.next() => {
if let Some(result) = res {
let str = serde_json::to_string(&result).map_err(InvokeError::from_error)?;
subscription_webview.emit_to(EventTarget::Webview{label: subscription_webview.label().into()},event_id, str)?;
}else {
break;
}
},
else => {
break;
}
}
}
} else {
while let Some(result) = stream.next().await {
let str = serde_json::to_string(&result).map_err(InvokeError::from_error)?;
subscription_webview.emit_to(EventTarget::Webview{label: subscription_webview.label().into()},event_id, str)?;
}
}
subscription_webview.emit_to(EventTarget::Webview{label: subscription_webview.label().into()}, event_id, Option::<()>::None)?;
Ok(())
}),
cmd => invoke.resolver.reject(format!(
"Invalid endpoint \"{}\". Valid endpoints are: \"graphql\", \"subscriptions\".",
cmd
)),
}
true
}
fn window_created(&mut self, window: Window<R>) {
(self.on_window_ready)(window)
}
fn webview_created(&mut self, webview: tauri::Webview<R>) {
(self.on_webview_ready)(webview)
}
fn on_navigation(&mut self, webview: &tauri::Webview<R>, url: &tauri::Url) -> bool {
(self.on_navigation)(webview, url)
}
}