Improve local filetree view state and sidebar performance
This commit is contained in:
@@ -19,7 +19,7 @@ use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::time::{interval, MissedTickBehavior};
|
||||
use tokio::time::{interval, timeout, MissedTickBehavior};
|
||||
|
||||
type BoxedEventStream =
|
||||
Pin<Box<dyn futures_util::Stream<Item = Result<SseEvent, Infallible>> + Send>>;
|
||||
@@ -177,18 +177,38 @@ async fn build_tree_live_stream(
|
||||
|
||||
loop {
|
||||
match subscription.receiver.recv().await {
|
||||
Ok(_watcher_payload) => {
|
||||
// Rebuild full snapshot on any filesystem change
|
||||
if let Some(resync_payload) =
|
||||
rebuild_tree_resync_payload(&root_uri, &workspace_id)
|
||||
{
|
||||
Ok(watcher_payload) => {
|
||||
let mut watcher_payloads = vec![watcher_payload];
|
||||
loop {
|
||||
match timeout(Duration::from_millis(120), subscription.receiver.recv())
|
||||
.await
|
||||
{
|
||||
Ok(Ok(next_payload)) => watcher_payloads.push(next_payload),
|
||||
Ok(Err(RecvError::Lagged(_))) => continue,
|
||||
Ok(Err(RecvError::Closed)) => return None,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
if let Some(batch_payload) = build_local_folder_watch_batch_payload(
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
watcher_payloads,
|
||||
) {
|
||||
return Some((
|
||||
Ok(stream_event("resync", &resync_payload)),
|
||||
Ok(stream_event("watch_batch", &batch_payload)),
|
||||
(None, subscription, root_uri, workspace_id),
|
||||
));
|
||||
}
|
||||
// Snapshot load failed — continue waiting for next change
|
||||
continue;
|
||||
let error_payload = build_tree_live_error_payload(
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
"tree_live_watch_batch_failed",
|
||||
"local folder watcher batch payload missing paths",
|
||||
);
|
||||
return Some((
|
||||
Ok(stream_event("tree_error", &error_payload)),
|
||||
(None, subscription, root_uri, workspace_id),
|
||||
));
|
||||
}
|
||||
Err(RecvError::Lagged(_)) => continue,
|
||||
Err(RecvError::Closed) => return None,
|
||||
@@ -276,6 +296,96 @@ fn rebuild_tree_resync_payload(root_uri: &str, workspace_id: &str) -> Option<Val
|
||||
))
|
||||
}
|
||||
|
||||
fn parent_relative_path_for_watch_path(relative_path: &str) -> String {
|
||||
let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
|
||||
if normalized.is_empty() || normalized == "." {
|
||||
return String::new();
|
||||
}
|
||||
normalized
|
||||
.rsplit_once('/')
|
||||
.map(|(parent, _)| parent.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn build_local_folder_watch_batch_payload(
|
||||
root_uri: &str,
|
||||
workspace_id: &str,
|
||||
watcher_payloads: Vec<Value>,
|
||||
) -> Option<Value> {
|
||||
let revision = local_folder_watch_revision(root_uri).ok()?;
|
||||
let mut changed_paths = Vec::new();
|
||||
let mut affected_parents = Vec::new();
|
||||
let mut event_kinds = Vec::new();
|
||||
let mut seen_paths = std::collections::BTreeSet::new();
|
||||
let mut seen_parents = std::collections::BTreeSet::new();
|
||||
let mut seen_kinds = std::collections::BTreeSet::new();
|
||||
for payload in watcher_payloads {
|
||||
let relative_path = payload
|
||||
.get("relativePath")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let event_kind = payload
|
||||
.get("eventKind")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("unknown");
|
||||
if seen_paths.insert(relative_path.to_string()) {
|
||||
changed_paths.push(json!({
|
||||
"relativePath": relative_path,
|
||||
"kind": event_kind,
|
||||
}));
|
||||
}
|
||||
if seen_kinds.insert(event_kind.to_string()) {
|
||||
event_kinds.push(event_kind.to_string());
|
||||
}
|
||||
let parent = parent_relative_path_for_watch_path(relative_path);
|
||||
if seen_parents.insert(parent.clone()) {
|
||||
affected_parents.push(json!({
|
||||
"relativePath": parent,
|
||||
"reason": "child-watch",
|
||||
}));
|
||||
}
|
||||
}
|
||||
if changed_paths.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(json!({
|
||||
"schema": "mnote.local_folder_watch_batch.v1",
|
||||
"kind": "watch_batch",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"workspaceId": workspace_id,
|
||||
"revision": revision.revision,
|
||||
"watchRevision": revision,
|
||||
"changedPaths": changed_paths,
|
||||
"affectedParents": affected_parents,
|
||||
"eventKinds": event_kinds,
|
||||
"fallbackResync": false,
|
||||
}))
|
||||
}
|
||||
|
||||
fn build_tree_live_error_payload(
|
||||
root_uri: &str,
|
||||
workspace_id: &str,
|
||||
code: &str,
|
||||
message: &str,
|
||||
) -> Value {
|
||||
json!({
|
||||
"schema": "mnote.tree_live_error.v1",
|
||||
"kind": "error",
|
||||
"phase": "tree_live_resync",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"workspaceId": workspace_id,
|
||||
"code": code,
|
||||
"message": message,
|
||||
"fallbackResync": true,
|
||||
"revision": system_time_ms(SystemTime::now()).to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_tree_snapshot_payload(
|
||||
root_uri: &str,
|
||||
workspace_id: &str,
|
||||
@@ -548,4 +658,66 @@ mod tests {
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_folder_watch_batch_payload_declares_changed_paths_and_parents() {
|
||||
let root = test_root("tree-live-watch-batch");
|
||||
std::fs::create_dir_all(root.join("docs")).expect("create docs");
|
||||
std::fs::write(root.join("docs/README.md"), "# Initial\n").expect("write initial");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
let workspace_id =
|
||||
local_workspace_id_from_root_uri(&root_uri).expect("resolve local workspace id");
|
||||
|
||||
let payload = build_local_folder_watch_batch_payload(
|
||||
&root_uri,
|
||||
&workspace_id,
|
||||
vec![
|
||||
json!({
|
||||
"relativePath": "docs/README.md",
|
||||
"eventKind": "Modify(Data)",
|
||||
}),
|
||||
json!({
|
||||
"relativePath": "docs/New.md",
|
||||
"eventKind": "Create(File)",
|
||||
}),
|
||||
],
|
||||
)
|
||||
.expect("watch batch payload");
|
||||
|
||||
assert_eq!(payload["schema"], "mnote.local_folder_watch_batch.v1");
|
||||
assert_eq!(payload["kind"], "watch_batch");
|
||||
assert_eq!(payload["fallbackResync"], false);
|
||||
assert_eq!(payload["changedPaths"].as_array().map(Vec::len), Some(2));
|
||||
assert!(
|
||||
payload["affectedParents"]
|
||||
.as_array()
|
||||
.expect("affected parents")
|
||||
.iter()
|
||||
.any(|parent| parent["relativePath"].as_str() == Some("docs")
|
||||
&& parent["reason"].as_str() == Some("child-watch")),
|
||||
"watch batch 应声明 docs affected parent: {payload}"
|
||||
);
|
||||
assert!(payload["eventKinds"]
|
||||
.as_array()
|
||||
.expect("event kinds")
|
||||
.iter()
|
||||
.any(|kind| kind.as_str() == Some("Modify(Data)")));
|
||||
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_live_error_payload_is_structured() {
|
||||
let payload = build_tree_live_error_payload(
|
||||
"file:///test",
|
||||
"local:test",
|
||||
"tree_live_resync_failed",
|
||||
"failed",
|
||||
);
|
||||
|
||||
assert_eq!(payload["schema"], "mnote.tree_live_error.v1");
|
||||
assert_eq!(payload["phase"], "tree_live_resync");
|
||||
assert_eq!(payload["fallbackResync"], true);
|
||||
assert_eq!(payload["code"], "tree_live_resync_failed");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user