Skip to content

Commit 2ca5112

Browse files
authored
Improve plugin source modeling and runtime dedup (#414)
1 parent 2d99e26 commit 2ca5112

13 files changed

Lines changed: 210 additions & 44 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- Tag safety: app releases use `v*` tags and CLI releases use `yaak-cli-*` tags; always confirm which one is requested before retagging.
2+
- Do not commit, push, or tag without explicit approval

crates-tauri/yaak-app/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use yaak_mac_window::AppHandleMacWindowExt;
3838
use yaak_models::models::{
3939
AnyModel, CookieJar, Environment, GrpcConnection, GrpcConnectionState, GrpcEvent,
4040
GrpcEventType, HttpRequest, HttpResponse, HttpResponseEvent, HttpResponseState, Plugin,
41-
Workspace, WorkspaceMeta,
41+
PluginSource, Workspace, WorkspaceMeta,
4242
};
4343
use yaak_models::util::{BatchUpsertResult, UpdateSource, get_workspace_export_resources};
4444
use yaak_plugins::events::{
@@ -1354,7 +1354,13 @@ async fn cmd_install_plugin<R: Runtime>(
13541354
window: WebviewWindow<R>,
13551355
) -> YaakResult<Plugin> {
13561356
let plugin = app_handle.db().upsert_plugin(
1357-
&Plugin { directory: directory.into(), url, enabled: true, ..Default::default() },
1357+
&Plugin {
1358+
directory: directory.into(),
1359+
url,
1360+
enabled: true,
1361+
source: PluginSource::Filesystem,
1362+
..Default::default()
1363+
},
13581364
&UpdateSource::from_window_label(window.label()),
13591365
)?;
13601366

crates-tauri/yaak-app/src/models_ext.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use yaak_models::error::Result;
1515
use yaak_models::models::{AnyModel, GraphQlIntrospection, GrpcEvent, Settings, WebsocketEvent};
1616
use yaak_models::query_manager::QueryManager;
1717
use yaak_models::util::UpdateSource;
18+
use yaak_plugins::manager::PluginManager;
1819

1920
const MODEL_CHANGES_RETENTION_HOURS: i64 = 1;
2021
const MODEL_CHANGES_POLL_INTERVAL_MS: u64 = 1000;
@@ -255,23 +256,32 @@ pub(crate) fn models_upsert_graphql_introspection<R: Runtime>(
255256
}
256257

257258
#[tauri::command]
258-
pub(crate) fn models_workspace_models<R: Runtime>(
259+
pub(crate) async fn models_workspace_models<R: Runtime>(
259260
window: WebviewWindow<R>,
260261
workspace_id: Option<&str>,
262+
plugin_manager: State<'_, PluginManager>,
261263
) -> Result<String> {
262-
let db = window.db();
263264
let mut l: Vec<AnyModel> = Vec::new();
264265

265-
// Add the settings
266-
l.push(db.get_settings().into());
266+
// Add the global models
267+
{
268+
let db = window.db();
269+
l.push(db.get_settings().into());
270+
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
271+
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
272+
}
273+
274+
let plugins = {
275+
let db = window.db();
276+
db.list_plugins()?
277+
};
267278

268-
// Add global models
269-
l.append(&mut db.list_workspaces()?.into_iter().map(Into::into).collect());
270-
l.append(&mut db.list_key_values()?.into_iter().map(Into::into).collect());
271-
l.append(&mut db.list_plugins()?.into_iter().map(Into::into).collect());
279+
let plugins = plugin_manager.resolve_plugins_for_runtime_from_db(plugins).await;
280+
l.append(&mut plugins.into_iter().map(Into::into).collect());
272281

273282
// Add the workspace children
274283
if let Some(wid) = workspace_id {
284+
let db = window.db();
275285
l.append(&mut db.list_cookie_jars(wid)?.into_iter().map(Into::into).collect());
276286
l.append(&mut db.list_environments_ensure_base(wid)?.into_iter().map(Into::into).collect());
277287
l.append(&mut db.list_folders(wid)?.into_iter().map(Into::into).collect());

crates/yaak-models/bindings/gen_models.ts

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
ALTER TABLE plugins
2+
ADD COLUMN source TEXT DEFAULT 'filesystem' NOT NULL;
3+
4+
-- Existing registry installs have a URL; classify them first.
5+
UPDATE plugins
6+
SET source = 'registry'
7+
WHERE url IS NOT NULL;
8+
9+
-- Best-effort bundled backfill for legacy rows.
10+
UPDATE plugins
11+
SET source = 'bundled'
12+
WHERE source = 'filesystem'
13+
AND (
14+
-- Normalize separators so this also works for Windows paths.
15+
replace(directory, '\', '/') LIKE '%/vendored/plugins/%'
16+
OR replace(directory, '\', '/') LIKE '%/vendored-plugins/%'
17+
);
18+
19+
-- Keep one row per exact directory before adding uniqueness.
20+
-- Tie-break by recency.
21+
WITH ranked AS (SELECT id,
22+
ROW_NUMBER() OVER (
23+
PARTITION BY directory
24+
ORDER BY updated_at DESC,
25+
created_at DESC
26+
) AS row_num
27+
FROM plugins)
28+
DELETE
29+
FROM plugins
30+
WHERE id IN (SELECT id FROM ranked WHERE row_num > 1);
31+
32+
CREATE UNIQUE INDEX IF NOT EXISTS idx_plugins_directory_unique
33+
ON plugins (directory);

crates/yaak-models/src/models.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,46 @@ pub struct Plugin {
20742074
pub directory: String,
20752075
pub enabled: bool,
20762076
pub url: Option<String>,
2077+
pub source: PluginSource,
2078+
}
2079+
2080+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2081+
#[serde(rename_all = "snake_case")]
2082+
#[ts(export, export_to = "gen_models.ts")]
2083+
pub enum PluginSource {
2084+
Bundled,
2085+
Filesystem,
2086+
Registry,
2087+
}
2088+
2089+
impl FromStr for PluginSource {
2090+
type Err = crate::error::Error;
2091+
2092+
fn from_str(s: &str) -> Result<Self> {
2093+
match s {
2094+
"bundled" => Ok(Self::Bundled),
2095+
"filesystem" => Ok(Self::Filesystem),
2096+
"registry" => Ok(Self::Registry),
2097+
_ => Ok(Self::default()),
2098+
}
2099+
}
2100+
}
2101+
2102+
impl Display for PluginSource {
2103+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2104+
let str = match self {
2105+
PluginSource::Bundled => "bundled".to_string(),
2106+
PluginSource::Filesystem => "filesystem".to_string(),
2107+
PluginSource::Registry => "registry".to_string(),
2108+
};
2109+
write!(f, "{}", str)
2110+
}
2111+
}
2112+
2113+
impl Default for PluginSource {
2114+
fn default() -> Self {
2115+
Self::Filesystem
2116+
}
20772117
}
20782118

20792119
impl UpsertModelInfo for Plugin {
@@ -2109,6 +2149,7 @@ impl UpsertModelInfo for Plugin {
21092149
(Directory, self.directory.into()),
21102150
(Url, self.url.into()),
21112151
(Enabled, self.enabled.into()),
2152+
(Source, self.source.to_string().into()),
21122153
])
21132154
}
21142155

@@ -2119,6 +2160,7 @@ impl UpsertModelInfo for Plugin {
21192160
PluginIden::Directory,
21202161
PluginIden::Url,
21212162
PluginIden::Enabled,
2163+
PluginIden::Source,
21222164
]
21232165
}
21242166

@@ -2135,6 +2177,7 @@ impl UpsertModelInfo for Plugin {
21352177
url: row.get("url")?,
21362178
directory: row.get("directory")?,
21372179
enabled: row.get("enabled")?,
2180+
source: PluginSource::from_str(row.get::<_, String>("source")?.as_str()).unwrap(),
21382181
})
21392182
}
21402183
}

crates/yaak-models/src/queries/plugins.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ impl<'a> DbContext<'a> {
2626
}
2727

2828
pub fn upsert_plugin(&self, plugin: &Plugin, source: &UpdateSource) -> Result<Plugin> {
29-
self.upsert(plugin, source)
29+
let mut plugin_to_upsert = plugin.clone();
30+
if let Some(existing) = self.get_plugin_by_directory(&plugin.directory) {
31+
plugin_to_upsert.id = existing.id;
32+
}
33+
self.upsert(&plugin_to_upsert, source)
3034
}
3135
}

crates/yaak-plugins/src/api.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
77
use std::path::Path;
88
use std::str::FromStr;
99
use ts_rs::TS;
10-
use yaak_models::models::Plugin;
10+
use yaak_models::models::{Plugin, PluginSource};
1111

1212
/// Get plugin info from the registry.
1313
pub async fn get_plugin(
@@ -58,7 +58,7 @@ pub async fn check_plugin_updates(
5858
) -> Result<PluginUpdatesResponse> {
5959
let name_versions: Vec<PluginNameVersion> = plugins
6060
.into_iter()
61-
.filter(|p| p.url.is_some()) // Only check plugins with URLs (from registry)
61+
.filter(|p| matches!(p.source, PluginSource::Registry)) // Only check registry-installed plugins
6262
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
6363
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
6464
Err(e) => {

crates/yaak-plugins/src/install.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use log::info;
99
use std::fs::{create_dir_all, remove_dir_all};
1010
use std::io::Cursor;
1111
use std::sync::Arc;
12-
use yaak_models::models::Plugin;
12+
use yaak_models::models::{Plugin, PluginSource};
1313
use yaak_models::query_manager::QueryManager;
1414
use yaak_models::util::UpdateSource;
1515

@@ -78,6 +78,7 @@ pub async fn download_and_install(
7878
directory: plugin_dir_str.clone(),
7979
enabled: true,
8080
url: Some(plugin_version.url.clone()),
81+
source: PluginSource::Registry,
8182
..Default::default()
8283
},
8384
&UpdateSource::Background,

crates/yaak-plugins/src/manager.rs

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ use crate::events::{
2121
use crate::native_template_functions::{template_function_keyring, template_function_secure};
2222
use crate::nodejs::start_nodejs_plugin_runtime;
2323
use crate::plugin_handle::PluginHandle;
24+
use crate::plugin_meta::get_plugin_meta;
2425
use crate::server_ws::PluginRuntimeServerWebsocket;
2526
use log::{error, info, warn};
26-
use std::collections::HashMap;
27+
use std::collections::{HashMap, HashSet};
2728
use std::env;
2829
use std::path::{Path, PathBuf};
2930
use std::sync::Arc;
@@ -33,7 +34,7 @@ use tokio::net::TcpListener;
3334
use tokio::sync::mpsc::error::TrySendError;
3435
use tokio::sync::{Mutex, mpsc, oneshot};
3536
use tokio::time::{Instant, timeout};
36-
use yaak_models::models::Plugin;
37+
use yaak_models::models::{Plugin, PluginSource};
3738
use yaak_models::query_manager::QueryManager;
3839
use yaak_models::util::{UpdateSource, generate_id};
3940
use yaak_templates::error::Error::RenderError;
@@ -162,13 +163,14 @@ impl PluginManager {
162163

163164
let bundled_dirs = plugin_manager.list_bundled_plugin_dirs().await?;
164165
let db = query_manager.connect();
165-
for dir in bundled_dirs {
166-
if db.get_plugin_by_directory(&dir).is_none() {
166+
for dir in &bundled_dirs {
167+
if db.get_plugin_by_directory(dir).is_none() {
167168
db.upsert_plugin(
168169
&Plugin {
169-
directory: dir,
170+
directory: dir.clone(),
170171
enabled: true,
171172
url: None,
173+
source: PluginSource::Bundled,
172174
..Default::default()
173175
},
174176
&UpdateSource::Background,
@@ -213,6 +215,57 @@ impl PluginManager {
213215
read_plugins_dir(&plugins_dir).await
214216
}
215217

218+
pub async fn resolve_plugins_for_runtime_from_db(&self, plugins: Vec<Plugin>) -> Vec<Plugin> {
219+
let bundled_dirs = match self.list_bundled_plugin_dirs().await {
220+
Ok(dirs) => dirs,
221+
Err(err) => {
222+
warn!("Failed to read bundled plugin dirs for resolution: {err:?}");
223+
Vec::new()
224+
}
225+
};
226+
self.resolve_plugins_for_runtime(plugins, bundled_dirs)
227+
}
228+
229+
/// Resolve the plugin set for the current runtime instance.
230+
///
231+
/// Rules:
232+
/// - Drop bundled rows that are not present in this instance's bundled directory list.
233+
/// - Deduplicate by plugin metadata name (fallback to directory key when metadata is unreadable).
234+
/// - Prefer sources in this order: filesystem > registry > bundled.
235+
/// - For same-source conflicts, prefer the most recently installed row (`created_at`).
236+
fn resolve_plugins_for_runtime(
237+
&self,
238+
plugins: Vec<Plugin>,
239+
bundled_dirs: Vec<String>,
240+
) -> Vec<Plugin> {
241+
let bundled_dir_set: HashSet<String> = bundled_dirs.into_iter().collect();
242+
let mut selected: HashMap<String, Plugin> = HashMap::new();
243+
244+
for plugin in plugins {
245+
if matches!(plugin.source, PluginSource::Bundled)
246+
&& !bundled_dir_set.contains(&plugin.directory)
247+
{
248+
continue;
249+
}
250+
251+
let key = match get_plugin_meta(Path::new(&plugin.directory)) {
252+
Ok(meta) => meta.name,
253+
Err(_) => format!("__dir__{}", plugin.directory),
254+
};
255+
256+
match selected.get(&key) {
257+
Some(existing) if !prefer_plugin(&plugin, existing) => {}
258+
_ => {
259+
selected.insert(key, plugin);
260+
}
261+
}
262+
}
263+
264+
let mut resolved = selected.into_values().collect::<Vec<_>>();
265+
resolved.sort_by(|a, b| b.created_at.cmp(&a.created_at));
266+
resolved
267+
}
268+
216269
pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> {
217270
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
218271
self.remove_plugin(plugin_context, &plugin).await
@@ -287,7 +340,8 @@ impl PluginManager {
287340
Ok(())
288341
}
289342

290-
/// Initialize all plugins from the provided list.
343+
/// Initialize all plugins from the provided DB list.
344+
/// Plugin candidates are resolved for this runtime instance before initialization.
291345
/// Returns a list of (plugin_directory, error_message) for any plugins that failed to initialize.
292346
pub async fn initialize_all_plugins(
293347
&self,
@@ -297,15 +351,18 @@ impl PluginManager {
297351
info!("Initializing all plugins");
298352
let start = Instant::now();
299353
let mut errors = Vec::new();
354+
let plugins = self.resolve_plugins_for_runtime_from_db(plugins).await;
355+
356+
// Rebuild runtime handles from scratch to avoid stale/duplicate handles.
357+
let existing_handles = { self.plugin_handles.lock().await.clone() };
358+
for plugin_handle in existing_handles {
359+
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
360+
error!("Failed to remove plugin {} {e:?}", plugin_handle.dir);
361+
errors.push((plugin_handle.dir.clone(), e.to_string()));
362+
}
363+
}
300364

301365
for plugin in plugins {
302-
// First remove the plugin if it exists and is enabled
303-
if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await {
304-
if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await {
305-
error!("Failed to remove plugin {} {e:?}", plugin.directory);
306-
continue;
307-
}
308-
}
309366
if let Err(e) = self.add_plugin(plugin_context, &plugin).await {
310367
warn!("Failed to add plugin {} {e:?}", plugin.directory);
311368
errors.push((plugin.directory.clone(), e.to_string()));
@@ -1063,6 +1120,24 @@ impl PluginManager {
10631120
}
10641121
}
10651122

1123+
fn source_priority(source: &PluginSource) -> i32 {
1124+
match source {
1125+
PluginSource::Filesystem => 3,
1126+
PluginSource::Registry => 2,
1127+
PluginSource::Bundled => 1,
1128+
}
1129+
}
1130+
1131+
fn prefer_plugin(candidate: &Plugin, existing: &Plugin) -> bool {
1132+
let candidate_priority = source_priority(&candidate.source);
1133+
let existing_priority = source_priority(&existing.source);
1134+
if candidate_priority != existing_priority {
1135+
return candidate_priority > existing_priority;
1136+
}
1137+
1138+
candidate.created_at > existing.created_at
1139+
}
1140+
10661141
async fn read_plugins_dir(dir: &PathBuf) -> Result<Vec<String>> {
10671142
let mut result = read_dir(dir).await?;
10681143
let mut dirs: Vec<String> = vec![];

0 commit comments

Comments
 (0)