diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ea4f0e4..170e326d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -81,7 +81,9 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Install yaak CLI - run: go install github.com/yaakapp/cli/cmd/yaakcli@latest + run: | + npm install -g @yaakapp/cli + yaakcli --version - name: Run lint run: npm run lint diff --git a/plugin-runtime-types/src/gen/InternalEventPayload.ts b/plugin-runtime-types/src/gen/InternalEventPayload.ts index 444b4eb1..a4d2226c 100644 --- a/plugin-runtime-types/src/gen/InternalEventPayload.ts +++ b/plugin-runtime-types/src/gen/InternalEventPayload.ts @@ -19,10 +19,12 @@ import type { ImportRequest } from "./ImportRequest"; import type { ImportResponse } from "./ImportResponse"; import type { PluginBootRequest } from "./PluginBootRequest"; import type { PluginBootResponse } from "./PluginBootResponse"; +import type { PluginReloadRequest } from "./PluginReloadRequest"; +import type { PluginReloadResponse } from "./PluginReloadResponse"; import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest"; import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse"; import type { SendHttpRequestRequest } from "./SendHttpRequestRequest"; import type { SendHttpRequestResponse } from "./SendHttpRequestResponse"; import type { ShowToastRequest } from "./ShowToastRequest"; -export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse; +export type InternalEventPayload = { "type": "boot_request" } & PluginBootRequest | { "type": "boot_response" } & PluginBootResponse | { "type": "reload_request" } & PluginReloadRequest | { "type": "reload_response" } & PluginReloadResponse | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & GetHttpRequestActionsRequest | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyResponse; diff --git a/plugin-runtime-types/src/gen/Plugin.ts b/plugin-runtime-types/src/gen/Plugin.ts index a3abb0d5..08773c6c 100644 --- a/plugin-runtime-types/src/gen/Plugin.ts +++ b/plugin-runtime-types/src/gen/Plugin.ts @@ -1,3 +1,3 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Plugin = { id: string, model: "plugin", createdAt: string, updatedAt: string, checkedAt: string | null, name: string, version: string, uri: string, enabled: boolean, }; +export type Plugin = { id: string, model: "plugin", createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, url: string | null, enabled: boolean, }; diff --git a/plugin-runtime-types/src/gen/PluginReloadRequest.ts b/plugin-runtime-types/src/gen/PluginReloadRequest.ts new file mode 100644 index 00000000..2bf23758 --- /dev/null +++ b/plugin-runtime-types/src/gen/PluginReloadRequest.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginReloadRequest = {}; diff --git a/plugin-runtime-types/src/gen/PluginReloadResponse.ts b/plugin-runtime-types/src/gen/PluginReloadResponse.ts new file mode 100644 index 00000000..44393138 --- /dev/null +++ b/plugin-runtime-types/src/gen/PluginReloadResponse.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type PluginReloadResponse = {}; diff --git a/plugin-runtime-types/src/index.ts b/plugin-runtime-types/src/index.ts index 66739769..ddd79b18 100644 --- a/plugin-runtime-types/src/index.ts +++ b/plugin-runtime-types/src/index.ts @@ -45,6 +45,8 @@ export * from './gen/KeyValue'; export * from './gen/Model'; export * from './gen/PluginBootRequest'; export * from './gen/PluginBootResponse'; +export * from './gen/PluginReloadRequest'; +export * from './gen/PluginReloadResponse'; export * from './gen/RenderHttpRequestRequest'; export * from './gen/RenderHttpRequestResponse'; export * from './gen/RenderPurpose'; diff --git a/plugin-runtime/src/PluginHandle.ts b/plugin-runtime/src/PluginHandle.ts index 912bec4f..d3c08e19 100644 --- a/plugin-runtime/src/PluginHandle.ts +++ b/plugin-runtime/src/PluginHandle.ts @@ -4,30 +4,35 @@ import { Worker } from 'node:worker_threads'; import { EventChannel } from './EventChannel'; export class PluginHandle { - readonly #worker: Worker; + #worker: Worker; constructor( readonly pluginDir: string, readonly pluginRefId: string, readonly events: EventChannel, ) { - const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs'); - this.#worker = new Worker(workerPath, { - workerData: { - pluginDir, - pluginRefId, - }, - }); - - this.#worker.on('message', (e) => this.events.emit(e)); - this.#worker.on('error', this.#handleError.bind(this)); - this.#worker.on('exit', this.#handleExit.bind(this)); + this.#worker = this.#createWorker(); } sendToWorker(event: InternalEvent) { this.#worker.postMessage(event); } + #createWorker(): Worker { + const workerPath = process.env.YAAK_WORKER_PATH ?? path.join(__dirname, 'index.worker.cjs'); + const worker = new Worker(workerPath, { + workerData: { pluginDir: this.pluginDir, pluginRefId: this.pluginRefId }, + }); + + worker.on('message', (e) => this.events.emit(e)); + worker.on('error', this.#handleError.bind(this)); + worker.on('exit', this.#handleExit.bind(this)); + + console.log('Created plugin worker for ', this.pluginDir); + + return worker; + } + async #handleError(err: Error) { console.error('Plugin errored', this.pluginDir, err); } @@ -36,7 +41,7 @@ export class PluginHandle { if (code === 0) { console.log('Plugin exited successfully', this.pluginDir); } else { - console.log('Plugin exited with error', code, this.pluginDir); + console.log('Plugin exited with status', code, this.pluginDir); } } } diff --git a/plugin-runtime/src/index.worker.ts b/plugin-runtime/src/index.worker.ts index 2fb9fe7d..e44b9a27 100644 --- a/plugin-runtime/src/index.worker.ts +++ b/plugin-runtime/src/index.worker.ts @@ -10,34 +10,31 @@ import { SendHttpRequestResponse, TemplateFunction, } from '@yaakapp/api'; -import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction'; +import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/HttpRequestActionPlugin'; import { TemplateFunctionPlugin } from '@yaakapp/api/lib/plugins/TemplateFunctionPlugin'; import interceptStdout from 'intercept-stdout'; import * as console from 'node:console'; -import { readFileSync } from 'node:fs'; +import { readFileSync, watch } from 'node:fs'; import path from 'node:path'; import * as util from 'node:util'; import { parentPort, workerData } from 'node:worker_threads'; -new Promise(async (resolve, reject) => { +async function initialize() { const { pluginDir, pluginRefId } = workerData; const pathPkg = path.join(pluginDir, 'package.json'); - // NOTE: Use POSIX join because require() needs forward slash const pathMod = path.posix.join(pluginDir, 'build', 'index.js'); - - let pkg: { [x: string]: any }; - try { - pkg = JSON.parse(readFileSync(pathPkg, 'utf8')); - } catch (err) { - // TODO: Do something better here - reject(err); - return; + async function importModule() { + const id = require.resolve(pathMod); + delete require.cache[id]; + return require(id); } + const pkg = JSON.parse(readFileSync(pathPkg, 'utf8')); + prefixStdout(`[plugin][${pkg.name}] %s`); - const mod = (await import(pathMod)).default ?? {}; + let mod = await importModule(); const capabilities: string[] = []; if (typeof mod.pluginHookExport === 'function') capabilities.push('export'); @@ -94,6 +91,18 @@ new Promise(async (resolve, reject) => { return promise as unknown as Promise; } + async function reloadModule() { + mod = await importModule(); + } + + // Reload plugin if JS or package.json changes + const cb = async () => { + await reloadModule(); + return sendPayload({ type: 'reload_response' }, null); + }; + watch(path.join(pathMod), cb); + watch(path.join(pathPkg), cb); + const ctx: Context = { clipboard: { async copyText(text) { @@ -248,6 +257,10 @@ new Promise(async (resolve, reject) => { return; } } + + if (payload.type === 'reload_request') { + await reloadModule(); + } } catch (err) { console.log('Plugin call threw exception', payload.type, err); // TODO: Return errors to server @@ -256,9 +269,9 @@ new Promise(async (resolve, reject) => { // No matches, so send back an empty response so the caller doesn't block forever sendEmpty(replyId); }); +} - resolve(); -}).catch((err) => { +initialize().catch((err) => { console.log('failed to boot plugin', err); }); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0ecde01d..15ef85f4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,6 +11,7 @@ use std::str::FromStr; use std::time::Duration; use base64::Engine; +use chrono::Utc; use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; @@ -61,6 +62,7 @@ use yaak_plugin_runtime::events::{ InternalEvent, InternalEventPayload, PluginBootResponse, RenderHttpRequestResponse, SendHttpRequestResponse, }; +use yaak_plugin_runtime::handle::PluginHandle; use yaak_templates::{Parser, Tokens}; mod analytics; @@ -1170,7 +1172,11 @@ async fn cmd_create_workspace(name: &str, w: WebviewWindow) -> Result, w: WebviewWindow) -> Result { +async fn cmd_create_plugin( + directory: &str, + url: Option, + w: WebviewWindow, +) -> Result { upsert_plugin( &w, Plugin { @@ -1456,6 +1462,12 @@ async fn cmd_list_plugins(w: WebviewWindow) -> Result, String> { list_plugins(&w).await.map_err(|e| e.to_string()) } +#[tauri::command] +async fn cmd_reload_plugins(plugin_manager: State<'_, PluginManager>) -> Result<(), String> { + plugin_manager.reload_all().await; + Ok(()) +} + #[tauri::command] async fn cmd_plugin_info( id: &str, @@ -1734,9 +1746,6 @@ pub fn run() { cmd_delete_http_response, cmd_delete_workspace, cmd_dismiss_notification, - cmd_parse_template, - cmd_template_tokens_to_string, - cmd_render_template, cmd_duplicate_grpc_request, cmd_duplicate_http_request, cmd_export_data, @@ -1752,7 +1761,6 @@ pub fn run() { cmd_grpc_go, cmd_grpc_reflect, cmd_http_request_actions, - cmd_template_functions, cmd_import_data, cmd_list_cookie_jars, cmd_list_environments, @@ -1763,16 +1771,21 @@ pub fn run() { cmd_list_http_requests, cmd_list_http_responses, cmd_list_plugins, - cmd_plugin_info, cmd_list_workspaces, cmd_metadata, cmd_new_nested_window, cmd_new_window, + cmd_parse_template, + cmd_plugin_info, + cmd_reload_plugins, + cmd_render_template, cmd_save_response, cmd_send_ephemeral_request, cmd_send_http_request, cmd_set_key_value, cmd_set_update_mode, + cmd_template_functions, + cmd_template_tokens_to_string, cmd_track_event, cmd_update_cookie_jar, cmd_update_environment, @@ -1996,17 +2009,25 @@ fn monitor_plugin_events(app_handle: &AppHandle) { while let Some(event) = rx.recv().await { let app_handle = app_handle.clone(); + let plugin = plugin_manager + .get_plugin(event.plugin_ref_id.as_str()) + .await + .unwrap(); // We might have recursive back-and-forth calls between app and plugin, so we don't // want to block here tauri::async_runtime::spawn(async move { - handle_plugin_event(&app_handle, &event).await; + handle_plugin_event(&app_handle, &event, &plugin).await; }); } }); } -async fn handle_plugin_event(app_handle: &AppHandle, event: &InternalEvent) { +async fn handle_plugin_event( + app_handle: &AppHandle, + event: &InternalEvent, + plugin_handle: &PluginHandle, +) { // info!("Got event to app {}", event.id); let response_event: Option = match event.clone().payload { InternalEventPayload::CopyTextRequest(req) => { @@ -2064,6 +2085,27 @@ async fn handle_plugin_event(app_handle: &AppHandle, event: &Inte }, )) } + InternalEventPayload::ReloadResponse(_) => { + let w = get_focused_window_no_lock(app_handle).expect("No focused window"); + let plugins = list_plugins(&w).await.unwrap(); + for plugin in plugins { + if plugin.directory != plugin_handle.dir { + continue; + } + + upsert_plugin( + &w, + Plugin { + // TODO: Add reloaded_at field to use instead + updated_at: Utc::now().naive_utc(), + ..plugin + }, + ) + .await + .unwrap(); + } + None + } InternalEventPayload::SendHttpRequestRequest(req) => { let w = get_focused_window_no_lock(app_handle).expect("No focused window"); let url = w.url().unwrap(); @@ -2124,7 +2166,6 @@ fn get_focused_window_no_lock(app_handle: &AppHandle) -> Option>>>, + pub(crate) boot_resp: Arc>>, +} + +impl PluginHandle { + pub async fn name(&self) -> String { + match &*self.boot_resp.lock().await { + None => "__NOT_BOOTED__".to_string(), + Some(r) => r.name.to_owned(), + } + } + + pub async fn info(&self) -> Option { + let resp = &*self.boot_resp.lock().await; + resp.clone() + } + + pub fn build_event_to_send( + &self, + payload: &InternalEventPayload, + reply_id: Option, + ) -> InternalEvent { + InternalEvent { + id: gen_id(), + plugin_ref_id: self.ref_id.clone(), + reply_id, + payload: payload.clone(), + } + } + + pub async fn reload(&self) -> crate::error::Result<()> { + let event = self.build_event_to_send(&InternalEventPayload::ReloadRequest(EmptyResponse{}), None); + self.send(&event).await + } + + pub async fn send(&self, event: &InternalEvent) -> crate::error::Result<()> { + // info!( + // "Sending event to plugin {} {:?}", + // event.id, + // self.name().await + // ); + self.to_plugin_tx + .lock() + .await + .send(Ok(EventStreamEvent { + event: serde_json::to_string(&event)?, + })) + .await?; + Ok(()) + } + + pub async fn boot(&self, resp: &PluginBootResponse) { + let mut boot_resp = self.boot_resp.lock().await; + *boot_resp = Some(resp.clone()); + } +} diff --git a/src-tauri/yaak_plugin_runtime/src/lib.rs b/src-tauri/yaak_plugin_runtime/src/lib.rs index 233a5636..9b636f0c 100644 --- a/src-tauri/yaak_plugin_runtime/src/lib.rs +++ b/src-tauri/yaak_plugin_runtime/src/lib.rs @@ -4,3 +4,5 @@ pub mod manager; mod nodejs; pub mod plugin; mod server; +pub mod handle; +mod util; diff --git a/src-tauri/yaak_plugin_runtime/src/manager.rs b/src-tauri/yaak_plugin_runtime/src/manager.rs index 2a0eb586..d1683610 100644 --- a/src-tauri/yaak_plugin_runtime/src/manager.rs +++ b/src-tauri/yaak_plugin_runtime/src/manager.rs @@ -15,6 +15,7 @@ use std::time::Duration; use tauri::{AppHandle, Runtime}; use tokio::sync::mpsc; use tokio::sync::watch::Sender; +use crate::handle::PluginHandle; pub struct PluginManager { kill_tx: Sender, @@ -38,6 +39,10 @@ impl PluginManager { PluginManager { kill_tx, server } } + pub async fn reload_all(&self) { + self.server.reload_plugins().await + } + pub async fn subscribe(&self) -> (String, mpsc::Receiver) { self.server.subscribe().await } @@ -68,6 +73,10 @@ impl PluginManager { self.server.plugin_by_dir(dir).await.ok()?.info().await } + pub async fn get_plugin(&self, ref_id: &str) -> Result { + self.server.plugin_by_ref_id(ref_id).await + } + pub async fn get_http_request_actions(&self) -> Result> { let reply_events = self .server diff --git a/src-tauri/yaak_plugin_runtime/src/server.rs b/src-tauri/yaak_plugin_runtime/src/server.rs index bdd0c4db..0b02f18d 100644 --- a/src-tauri/yaak_plugin_runtime/src/server.rs +++ b/src-tauri/yaak_plugin_runtime/src/server.rs @@ -1,7 +1,7 @@ -use rand::distributions::{Alphanumeric, DistString}; use std::collections::HashMap; use std::pin::Pin; use std::sync::Arc; +use log::warn; use tokio::sync::mpsc::Receiver; use tokio::sync::{mpsc, Mutex}; use tonic::codegen::tokio_stream::wrappers::ReceiverStream; @@ -10,8 +10,10 @@ use tonic::{Request, Response, Status, Streaming}; use crate::error::Error::PluginNotFoundErr; use crate::error::Result; -use crate::events::{PluginBootRequest, PluginBootResponse, InternalEvent, InternalEventPayload}; +use crate::events::{InternalEvent, InternalEventPayload, PluginBootRequest, PluginBootResponse}; +use crate::handle::PluginHandle; use crate::server::plugin_runtime::plugin_runtime_server::PluginRuntime; +use crate::util::gen_id; use plugin_runtime::EventStreamEvent; use yaak_models::queries::generate_id; @@ -22,62 +24,6 @@ pub mod plugin_runtime { type ResponseStream = Pin> + Send>>; -#[derive(Clone)] -pub struct PluginHandle { - dir: String, - to_plugin_tx: Arc>>>, - ref_id: String, - boot_resp: Arc>>, -} - -impl PluginHandle { - pub async fn name(&self) -> String { - match &*self.boot_resp.lock().await { - None => "__NOT_BOOTED__".to_string(), - Some(r) => r.name.to_owned(), - } - } - - pub async fn info(&self) -> Option { - let resp = &*self.boot_resp.lock().await; - resp.clone() - } - - pub fn build_event_to_send( - &self, - payload: &InternalEventPayload, - reply_id: Option, - ) -> InternalEvent { - InternalEvent { - id: gen_id(), - plugin_ref_id: self.ref_id.clone(), - reply_id, - payload: payload.clone(), - } - } - - pub async fn send(&self, event: &InternalEvent) -> Result<()> { - // info!( - // "Sending event to plugin {} {:?}", - // event.id, - // self.name().await - // ); - self.to_plugin_tx - .lock() - .await - .send(Ok(EventStreamEvent { - event: serde_json::to_string(&event)?, - })) - .await?; - Ok(()) - } - - pub async fn boot(&self, resp: &PluginBootResponse) { - let mut boot_resp = self.boot_resp.lock().await; - *boot_resp = Some(resp.clone()); - } -} - #[derive(Clone)] pub struct PluginRuntimeGrpcServer { plugin_ref_to_plugin: Arc>>, @@ -96,6 +42,15 @@ impl PluginRuntimeGrpcServer { } } + pub async fn plugins(&self) -> Vec { + self.plugin_ref_to_plugin + .lock() + .await + .iter() + .map(|p| p.1.to_owned()) + .collect::>() + } + pub async fn subscribe(&self) -> (String, Receiver) { let (tx, rx) = mpsc::channel(128); let rx_id = generate_id(); @@ -115,23 +70,15 @@ impl PluginRuntimeGrpcServer { pub async fn remove_plugin(&self, id: &str) { match self.plugin_ref_to_plugin.lock().await.remove(id) { - None => { - println!("Tried to remove non-existing plugin {}", id); - } - Some(plugin) => { - println!("Removed plugin {} {}", id, plugin.name().await); - } + None => println!("Tried to remove non-existing plugin {}", id), + Some(plugin) => println!("Removed plugin {} {}", id, plugin.name().await), }; } pub async fn boot_plugin(&self, id: &str, resp: &PluginBootResponse) { match self.plugin_ref_to_plugin.lock().await.get(id) { - None => { - println!("Tried booting non-existing plugin {}", id); - } - Some(plugin) => { - plugin.clone().boot(resp).await; - } + None => println!("Tried booting non-existing plugin {}", id), + Some(plugin) => plugin.clone().boot(resp).await, } } @@ -162,7 +109,7 @@ impl PluginRuntimeGrpcServer { Some(p) => Ok(p.to_owned()), } } - + pub async fn plugin_by_dir(&self, dir: &str) -> Result { let plugins = self.plugin_ref_to_plugin.lock().await; for p in plugins.values() { @@ -299,6 +246,14 @@ impl PluginRuntimeGrpcServer { Ok(events) } + pub async fn reload_plugins(&self) { + for (_, plugin) in self.plugin_ref_to_plugin.lock().await.clone() { + if let Err(e) = plugin.reload().await { + warn!("Failed to reload plugin {} {}", plugin.dir, e) + } + } + } + async fn load_plugins( &self, to_plugin_tx: mpsc::Sender>, @@ -396,7 +351,3 @@ impl PluginRuntime for PluginRuntimeGrpcServer { )) } } - -fn gen_id() -> String { - Alphanumeric.sample_string(&mut rand::thread_rng(), 5) -} diff --git a/src-tauri/yaak_plugin_runtime/src/util.rs b/src-tauri/yaak_plugin_runtime/src/util.rs new file mode 100644 index 00000000..6145548a --- /dev/null +++ b/src-tauri/yaak_plugin_runtime/src/util.rs @@ -0,0 +1,5 @@ +use rand::distributions::{Alphanumeric, DistString}; + +pub fn gen_id() -> String { + Alphanumeric.sample_string(&mut rand::thread_rng(), 5) +} diff --git a/src-web/hooks/useHttpRequestActions.ts b/src-web/hooks/useHttpRequestActions.ts index 368e1a75..3189e4f2 100644 --- a/src-web/hooks/useHttpRequestActions.ts +++ b/src-web/hooks/useHttpRequestActions.ts @@ -5,10 +5,12 @@ import type { HttpRequest, } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; +import { usePlugins } from './usePlugins'; export function useHttpRequestActions() { + const plugins = usePlugins(); const httpRequestActions = useQuery({ - queryKey: ['http_request_actions'], + queryKey: ['http_request_actions', plugins.map((p) => p.updatedAt)], refetchOnWindowFocus: false, queryFn: async () => { const responses = (await invokeCmd( diff --git a/src-web/hooks/usePlugins.ts b/src-web/hooks/usePlugins.ts index a8196e22..0cb9f2ab 100644 --- a/src-web/hooks/usePlugins.ts +++ b/src-web/hooks/usePlugins.ts @@ -3,6 +3,7 @@ import type { Plugin } from '@yaakapp/api'; import { atom, useAtomValue, useSetAtom } from 'jotai'; import { minPromiseMillis } from '../lib/minPromiseMillis'; import { listPlugins } from '../lib/store'; +import { invokeCmd } from '../lib/tauri'; const plugins = await listPlugins(); export const pluginsAtom = atom(plugins); @@ -11,12 +12,20 @@ export function usePlugins() { return useAtomValue(pluginsAtom); } +/** + * Reload all plugins and refresh the list of plugins + */ export function useRefreshPlugins() { const setPlugins = useSetAtom(pluginsAtom); return useMutation({ mutationKey: ['refresh_plugins'], mutationFn: async () => { - const plugins = await minPromiseMillis(listPlugins()); + const plugins = await minPromiseMillis( + (async function () { + await invokeCmd('cmd_reload_plugins'); + return listPlugins(); + })(), + ); setPlugins(plugins); }, }); diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index ca20cf77..2b12182e 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -56,6 +56,7 @@ type TauriCmd = | 'cmd_parse_template' | 'cmd_plugin_info' | 'cmd_render_template' + | 'cmd_reload_plugins' | 'cmd_save_response' | 'cmd_send_ephemeral_request' | 'cmd_send_http_request'