feat: add evidence search and stabilize pdf previews
- add document evidence parsing/search/open routes, Hermes tool wiring, local index settings/status, and the document-evidence skill plus design notes - fix PDF resource tabs by rendering PDFs inline with pdf.js canvases instead of iframe preview pages, release PDF documents on close, and document the fourth-PDF stall bug - keep PDF preview at 2x rendering while removing the previous lazy-load/placeholder direction, and make dev:hot bind loopback defaults externally reachable Verification: - node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js - node scripts/task-dev-hot-plan-test.js - cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar - cargo test -p mnote-web --manifest-path rust/Cargo.toml document_shell_returns_page_aggregate_snapshot - cargo build -p mnote-web --manifest-path rust/Cargo.toml - browser smoke: sequentially opened the four tea_seed_oil_cosmetic PDFs; fourth PDF rendered 15/15 canvases, iframeCount=0, browser errors=0
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
use crate::document_buffer_store::BufferStore;
|
||||
use crate::routes::{
|
||||
local_markdown_conflict_detection_key, local_workspace_id_from_root_uri,
|
||||
refresh_local_search_index_for_path,
|
||||
refresh_local_search_index_for_change_path_with_store,
|
||||
refresh_local_search_index_if_scheduled_due_with_store,
|
||||
};
|
||||
use control_plane::{ControlPlaneStore, SqliteControlPlaneStore};
|
||||
use notify::event::ModifyKind;
|
||||
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use serde_json::{json, Value};
|
||||
@@ -10,13 +12,15 @@ use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::{broadcast, mpsc, oneshot};
|
||||
use tokio::time::MissedTickBehavior;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct LocalFolderWatcherRegistry {
|
||||
inner: Arc<LocalFolderWatcherRegistryInner>,
|
||||
buffer_store: BufferStore,
|
||||
control_plane: Arc<dyn ControlPlaneStore>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for LocalFolderWatcherRegistry {
|
||||
@@ -28,12 +32,13 @@ impl std::fmt::Debug for LocalFolderWatcherRegistry {
|
||||
}
|
||||
|
||||
impl LocalFolderWatcherRegistry {
|
||||
pub fn new(buffer_store: BufferStore) -> Self {
|
||||
pub fn new(buffer_store: BufferStore, control_plane: Arc<SqliteControlPlaneStore>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(LocalFolderWatcherRegistryInner {
|
||||
entries: Mutex::new(HashMap::new()),
|
||||
}),
|
||||
buffer_store,
|
||||
control_plane,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +48,10 @@ impl LocalFolderWatcherRegistry {
|
||||
) -> Result<LocalFolderWatcherSubscription, String> {
|
||||
let key = canonical_root_uri(canonical_root);
|
||||
let buffer_store = self.buffer_store.clone();
|
||||
let channel = self
|
||||
.inner
|
||||
.get_or_create_channel(&key, canonical_root, buffer_store)?;
|
||||
let control_plane = self.control_plane.clone();
|
||||
let channel =
|
||||
self.inner
|
||||
.get_or_create_channel(&key, canonical_root, buffer_store, control_plane)?;
|
||||
channel.subscriber_count.fetch_add(1, Ordering::SeqCst);
|
||||
Ok(LocalFolderWatcherSubscription {
|
||||
receiver: channel.sender.subscribe(),
|
||||
@@ -73,6 +79,7 @@ impl LocalFolderWatcherRegistryInner {
|
||||
key: &str,
|
||||
canonical_root: &Path,
|
||||
buffer_store: BufferStore,
|
||||
control_plane: Arc<dyn ControlPlaneStore>,
|
||||
) -> Result<Arc<LocalFolderWatchChannel>, String> {
|
||||
if let Some(existing) = self
|
||||
.entries
|
||||
@@ -86,7 +93,12 @@ impl LocalFolderWatcherRegistryInner {
|
||||
|
||||
let channel = Arc::new(LocalFolderWatchChannel::new(
|
||||
key.to_string(),
|
||||
spawn_local_folder_watcher(key, canonical_root.to_path_buf(), buffer_store)?,
|
||||
spawn_local_folder_watcher(
|
||||
key,
|
||||
canonical_root.to_path_buf(),
|
||||
buffer_store,
|
||||
control_plane,
|
||||
)?,
|
||||
));
|
||||
|
||||
let mut entries = self.entries.lock().expect("registry lock");
|
||||
@@ -168,6 +180,7 @@ fn spawn_local_folder_watcher(
|
||||
root_uri: &str,
|
||||
canonical_root: PathBuf,
|
||||
buffer_store: BufferStore,
|
||||
control_plane: Arc<dyn ControlPlaneStore>,
|
||||
) -> Result<(broadcast::Sender<Value>, oneshot::Sender<()>), String> {
|
||||
let (event_sender, mut event_receiver) = mpsc::unbounded_channel::<notify::Result<Event>>();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
@@ -186,13 +199,23 @@ fn spawn_local_folder_watcher(
|
||||
let sender_for_task = sender.clone();
|
||||
let root_uri_for_task = root_uri.to_string();
|
||||
let buffer_store_for_task = buffer_store.clone();
|
||||
let control_plane_for_task = control_plane.clone();
|
||||
tokio::spawn(async move {
|
||||
let _watcher = watcher;
|
||||
let mut index_schedule_tick = tokio::time::interval(Duration::from_secs(60));
|
||||
index_schedule_tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut shutdown_rx => {
|
||||
return;
|
||||
}
|
||||
_ = index_schedule_tick.tick() => {
|
||||
refresh_local_search_index_for_schedule(
|
||||
control_plane_for_task.as_ref(),
|
||||
&canonical_root,
|
||||
&root_uri_for_task,
|
||||
);
|
||||
}
|
||||
maybe_result = event_receiver.recv() => {
|
||||
let Some(result) = maybe_result else {
|
||||
return;
|
||||
@@ -211,6 +234,7 @@ fn spawn_local_folder_watcher(
|
||||
continue;
|
||||
};
|
||||
refresh_local_search_index_for_event(
|
||||
control_plane_for_task.as_ref(),
|
||||
&canonical_root,
|
||||
&root_uri_for_task,
|
||||
&relative_path,
|
||||
@@ -298,11 +322,38 @@ fn spawn_local_folder_watcher(
|
||||
Ok((sender, shutdown_tx))
|
||||
}
|
||||
|
||||
fn refresh_local_search_index_for_event(root: &Path, root_uri: &str, relative_path: &str) {
|
||||
fn refresh_local_search_index_for_event(
|
||||
control_plane: &dyn ControlPlaneStore,
|
||||
root: &Path,
|
||||
root_uri: &str,
|
||||
relative_path: &str,
|
||||
) {
|
||||
let Ok(workspace_id) = local_workspace_id_from_root_uri(root_uri) else {
|
||||
return;
|
||||
};
|
||||
let _ = refresh_local_search_index_for_path(root, root_uri, &workspace_id, relative_path);
|
||||
let _ = refresh_local_search_index_for_change_path_with_store(
|
||||
control_plane,
|
||||
root,
|
||||
root_uri,
|
||||
&workspace_id,
|
||||
relative_path,
|
||||
);
|
||||
}
|
||||
|
||||
fn refresh_local_search_index_for_schedule(
|
||||
control_plane: &dyn ControlPlaneStore,
|
||||
root: &Path,
|
||||
root_uri: &str,
|
||||
) {
|
||||
let Ok(workspace_id) = local_workspace_id_from_root_uri(root_uri) else {
|
||||
return;
|
||||
};
|
||||
let _ = refresh_local_search_index_if_scheduled_due_with_store(
|
||||
control_plane,
|
||||
root,
|
||||
root_uri,
|
||||
&workspace_id,
|
||||
);
|
||||
}
|
||||
|
||||
fn canonical_root_uri(root: &Path) -> String {
|
||||
@@ -423,8 +474,11 @@ mod tests {
|
||||
LocalFolderWatcherRegistry,
|
||||
};
|
||||
use crate::document_buffer_store::BufferStore;
|
||||
use crate::routes::write_local_index_settings;
|
||||
use control_plane::SqliteControlPlaneStore;
|
||||
use notify::event::{AccessKind, CreateKind, DataChange, ModifyKind};
|
||||
use notify::EventKind;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_root(name: &str) -> std::path::PathBuf {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
@@ -441,7 +495,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_root_subscribers_share_single_watcher() {
|
||||
let registry = LocalFolderWatcherRegistry::new(BufferStore::new());
|
||||
let control_plane =
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("init control plane"));
|
||||
let registry = LocalFolderWatcherRegistry::new(BufferStore::new(), control_plane);
|
||||
let root = test_root("shared");
|
||||
|
||||
let first = registry.subscribe(&root).expect("first subscription");
|
||||
@@ -462,7 +518,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn different_roots_create_independent_watchers() {
|
||||
let registry = LocalFolderWatcherRegistry::new(BufferStore::new());
|
||||
let control_plane =
|
||||
Arc::new(SqliteControlPlaneStore::in_memory().expect("init control plane"));
|
||||
let registry = LocalFolderWatcherRegistry::new(BufferStore::new(), control_plane);
|
||||
let first_root = test_root("first");
|
||||
let second_root = test_root("second");
|
||||
|
||||
@@ -532,7 +590,10 @@ mod tests {
|
||||
.expect("write watched");
|
||||
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
refresh_local_search_index_for_event(&root, &root_uri, "docs/watched.md");
|
||||
let control_plane = SqliteControlPlaneStore::in_memory().expect("init control plane");
|
||||
write_local_index_settings(&root, &[String::from(".")], None, None, None, Some(true))
|
||||
.expect("enable run-on-change indexing");
|
||||
refresh_local_search_index_for_event(&control_plane, &root, &root_uri, "docs/watched.md");
|
||||
|
||||
let index_path = root.join(".mnote").join("index").join("search-index.json");
|
||||
let index = std::fs::read_to_string(&index_path).expect("index exists");
|
||||
|
||||
Reference in New Issue
Block a user