- Promoted sync-related logging from Debug/Trace to Info level across multiple files, ensuring visibility of changes in the Studio output console. - Added a new plan for expanding the connected mode test suite to cover multi-step scenarios and round-trip identity verification. - Introduced a plan to fix stale tree desynchronization by implementing a validation mechanism on plugin connection. - Created a plan to bump sync logging to Info level, detailing todos for various components in the plugin. This update improves the clarity of logs and enhances testing coverage for connected mode scenarios.
6.9 KiB
| name | overview | todos | isProject | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Fix stale tree desync | Add a tree validation mechanism that re-snapshots the project tree from the real filesystem when a plugin connects, ensuring the in-memory RojoTree never serves stale data even when VFS watcher events are missed. |
|
false |
Fix Stale Tree Desync on Plugin Connect
Problem
When a plugin connects (especially in one-shot mode), the server's in-memory RojoTree can be stale if the VFS file watcher missed external filesystem changes. The tree is built once on startup and only updated via VFS events through ChangeProcessor. If events are missed (watcher buffer overflow, rapid batch operations like git, OS-level quirks), the tree silently drifts from reality. The plugin reads this stale tree via GET /api/read/:id, computes its diff against Studio, and sees "no changes" even though the filesystem is different.
Rebooting rojo serve forces a fresh read and fixes it, confirming the issue is purely stale in-memory state.
Solution: Tree Validation on Plugin Connect
When a new plugin connection is established (WebSocket handshake), trigger a validation pass that re-snapshots the root project path from the real filesystem, diffs against the current tree, and applies any corrections. This is essentially what ServeSession::new does on startup, but incremental and without restart.
Key Files
[src/serve_session.rs](src/serve_session.rs)-- Owns the tree, VFS, and project. Addvalidate_tree()method here.[src/change_processor.rs](src/change_processor.rs)-- Hasapply_patches()which re-snapshots a path and patches the tree. Reuse this pattern.[src/web/api.rs](src/web/api.rs)-- WebSocket connection handler. Call validation before serving the tree.[src/snapshot/patch_compute.rs](src/snapshot/patch_compute.rs)--compute_patch_setused to diff snapshots.
Implementation
1. Add validate_tree() to ServeSession
In [src/serve_session.rs](src/serve_session.rs), add a public method that:
- Takes the root project's start path (
self.root_project.folder_location()) - Locks the VFS and tree
- Calls
snapshot_from_vfs(&instance_context, &vfs, start_path)to get a fresh snapshot from disk - Calls
compute_patch_set(snapshot, &tree, root_id)to diff against the current tree - If the patch is non-empty, calls
apply_patch_set(&mut tree, patch_set)and pushes the result to the message queue
This reuses the same functions that ServeSession::new uses for initial tree construction. The VFS reads directly from the filesystem (no cache), so the snapshot will reflect the real state.
A compile-time constant at the top of serve_session.rs controls whether validation runs:
/// Set to `false` to skip tree validation on plugin connect (useful for testing).
const VALIDATE_TREE_ON_CONNECT: bool = true;
The method itself:
pub fn validate_tree(&self) -> Vec<AppliedPatchSet> {
if !VALIDATE_TREE_ON_CONNECT {
log::debug!("Tree validation skipped (VALIDATE_TREE_ON_CONNECT = false)");
return Vec::new();
}
let start = std::time::Instant::now();
let start_path = self.root_project.folder_location();
let instance_context = InstanceContext::new();
let snapshot = match snapshot_from_vfs(&instance_context, &self.vfs, start_path) {
Ok(s) => s,
Err(e) => {
log::error!("Tree validation snapshot error: {:?}", e);
return Vec::new();
}
};
let mut tree = self.tree.lock().unwrap();
let root_id = tree.get_root_id();
let patch_set = compute_patch_set(snapshot, &tree, root_id);
if patch_set.is_empty() {
log::info!("Tree validation complete (no corrections needed) in {:.1?}", start.elapsed());
return Vec::new();
}
log::info!("Tree validation found stale state, applying corrections");
let applied = apply_patch_set(&mut tree, patch_set);
log::info!("Tree validation complete in {:.1?}", start.elapsed());
vec![applied]
}
Note: The Vfs struct wraps the backend with a mutex internally (self.inner.lock()), so we access it via self.vfs (which is Arc<Vfs>) without needing an explicit lock. The exact locking pattern will follow the existing code style.
2. Trigger Validation on WebSocket Connect
In [src/web/api.rs](src/web/api.rs), the WebSocket handler establishes a new connection. Before entering the message loop (or right after the handshake), call validate_tree() and broadcast any corrections:
// In the WebSocket handler, after handshake:
let corrections = self.serve_session.validate_tree();
if !corrections.is_empty() {
self.serve_session.message_queue().push_messages(&corrections);
}
This ensures the tree is consistent with disk before the plugin starts receiving data.
3. Broadcast Corrections to Already-Connected Plugins
If multiple plugins are connected, the corrections are pushed to the message queue, which broadcasts to all connected WebSocket clients. This ensures all plugins get the updated state.
Edge Cases
- Large projects: Full re-snapshot could add 100-500ms latency on connect for very large projects. This is acceptable for one-shot mode (happens once) and live mode (happens on reconnect). Can be optimized later with targeted path validation if needed.
- Concurrent access: The tree lock is held during validation. Other WebSocket handlers will block briefly. This matches the existing pattern in
ChangeProcessor::apply_patches. - No changes: If the tree is already fresh (common case),
compute_patch_setreturns an empty patch and no work is done. The overhead is onesnapshot_from_vfscall (directory walk + file stats). - VFS consistency: After validation, VFS events for the re-snapshotted paths will still fire (watches persist). The
compute_and_apply_changeshandler will process them as no-ops since the tree already matches.
What This Fixes
After this change, "no changes appearing" on plugin reconnect should not happen regardless of whether VFS events were missed. The tree is always refreshed from disk on connect, matching the behavior of a fresh rojo serve startup.
What This Does NOT Fix
The content "swap" issue (both directions firing during per-item selection) is a separate bug that needs further investigation. The stale tree fix ensures that even if a swap occurs, the developer can immediately reconnect and see the correct diff to recover.