From 2e49763b69e0930914a7989c586c385f1d074347 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 12 Feb 2026 02:13:58 -0600 Subject: [PATCH] Update snapshot files and enhance VFS event handling - Updated snapshot files to reflect changes in IDs for consistency across connected mode tests. - Improved VFS event processing in ChangeProcessor to handle multiple events more efficiently, ensuring consistent message structure across platforms. - Added merging functionality for patch sets to consolidate changes from various VFS events, enhancing synchronization reliability. These changes improve the accuracy of test snapshots and optimize VFS event management. --- ...ode__connected_clean_slug_clean_step1.snap | 2 +- ...ode__connected_clean_slug_clean_step2.snap | 2 +- ...ode__connected_clean_slug_clean_step3.snap | 15 ++++++-- ...ected_mode__connected_sequence_delete.snap | 16 ++------- ...ected_mode__connected_sequence_rename.snap | 13 ++++++- src/change_processor.rs | 35 ++++++++++++++----- src/snapshot/patch.rs | 17 +++++++++ tests/tests/connected_mode.rs | 4 +++ 8 files changed, 77 insertions(+), 27 deletions(-) diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step1.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step1.snap index e7cb7670..f716706b 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step1.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step1.snap @@ -13,6 +13,6 @@ body: ignoreUnknownInstances: false changedName: "Nor:mal" changedProperties: {} - id: id-3 + id: id-8 packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step2.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step2.snap index 10316350..2ced958e 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step2.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step2.snap @@ -13,6 +13,6 @@ body: ignoreUnknownInstances: false changedName: Normal changedProperties: {} - id: id-3 + id: id-8 packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step3.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step3.snap index 29eabe73..3ed1171a 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step3.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_clean_slug_clean_step3.snap @@ -5,9 +5,20 @@ expression: "packet.intern_and_redact(&mut redactions, ())" body: messageCursor: 3 messages: - - added: {} + - added: + id-13: + Children: [] + ClassName: ModuleScript + Id: id-13 + Metadata: + ignoreUnknownInstances: false + Name: Has/Slash + Parent: id-3 + Properties: + Source: + "!String": "-- Normal clean-named module\nreturn \"normal\"\n" removed: - - id-3 + - id-8 updated: [] packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_delete.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_delete.snap index 6b10289f..d15d01c3 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_delete.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_delete.snap @@ -5,19 +5,9 @@ expression: "packet.intern_and_redact(&mut redactions, ())" body: messageCursor: 4 messages: - - added: - id-15: - Children: [] - ClassName: ModuleScript - Id: id-15 - Metadata: - ignoreUnknownInstances: false - Name: Renamed - Parent: id-3 - Properties: - Source: - "!String": "-- edited script\nreturn 2" - removed: [] + - added: {} + removed: + - id-15 updated: [] packetType: messages sessionId: id-1 diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_rename.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_rename.snap index c2660214..1b769667 100644 --- a/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_rename.snap +++ b/rojo-test/serve-test-snapshots/end_to_end__tests__connected_mode__connected_sequence_rename.snap @@ -5,7 +5,18 @@ expression: "packet.intern_and_redact(&mut redactions, ())" body: messageCursor: 3 messages: - - added: {} + - added: + id-15: + Children: [] + ClassName: ModuleScript + Id: id-15 + Metadata: + ignoreUnknownInstances: false + Name: Renamed + Parent: id-3 + Properties: + Source: + "!String": "-- edited script\nreturn 2" removed: - id-14 updated: [] diff --git a/src/change_processor.rs b/src/change_processor.rs index d28a6411..81054557 100644 --- a/src/change_processor.rs +++ b/src/change_processor.rs @@ -120,8 +120,25 @@ impl ChangeProcessor { select! { recv(vfs_receiver) -> event => { - task.handle_vfs_event(event?); - task.process_pending_recoveries(); + let mut all_patches = task.handle_vfs_event(event?); + + // Drain any pending events that arrived during processing. + // This ensures that multi-event filesystem operations (e.g., + // rename = REMOVE + CREATE on Windows) produce a single + // batched message instead of separate per-event messages, + // giving consistent behavior across platforms. + while let Ok(event) = vfs_receiver.try_recv() { + all_patches.extend(task.handle_vfs_event(event)); + } + + all_patches.extend(task.process_pending_recoveries()); + + if !all_patches.is_empty() { + let merged = AppliedPatchSet::merge(all_patches); + if !merged.is_empty() { + task.message_queue.push_messages(&[merged]); + } + } // Schedule a reconciliation 200ms from now if one isn't pending. if reconcile_at.is_none() { @@ -470,7 +487,7 @@ impl JobThreadContext { applied_patches } - fn handle_vfs_event(&self, event: VfsEvent) { + fn handle_vfs_event(&self, event: VfsEvent) -> Vec { // Log EVERY VFS event at INFO level for diagnostics. // This is intentionally verbose — it is critical for debugging // file watcher desync issues (e.g., rapid delete+recreate). @@ -532,7 +549,7 @@ impl JobThreadContext { "VFS event SUPPRESSED (API syncback echo): {}", self.display_path(path) ); - return; + return Vec::new(); } } } @@ -673,15 +690,13 @@ impl JobThreadContext { log::info!("VFS event applied: no changes"); } - // Notify anyone listening to the message queue about the changes we - // just made. - self.message_queue.push_messages(&applied_patches); + applied_patches } /// Processes any pending recovery checks for paths that were recently /// removed. If a path has reappeared on the real filesystem after the /// recovery delay, we trigger a re-snapshot to bring the tree back in sync. - fn process_pending_recoveries(&self) { + fn process_pending_recoveries(&self) -> Vec { const RECOVERY_DELAY: Duration = Duration::from_millis(200); let ready: Vec = { @@ -701,6 +716,7 @@ impl JobThreadContext { ready }; + let mut all_patches = Vec::new(); for path in ready { if std::fs::metadata(&path).is_ok() { log::info!( @@ -718,7 +734,7 @@ impl JobThreadContext { total_removed, total_updated ); - self.message_queue.push_messages(&patches); + all_patches.extend(patches); } } else { log::info!( @@ -727,6 +743,7 @@ impl JobThreadContext { ); } } + all_patches } /// Re-snapshots the entire project from the real filesystem and patches diff --git a/src/snapshot/patch.rs b/src/snapshot/patch.rs index e8899357..fb984442 100644 --- a/src/snapshot/patch.rs +++ b/src/snapshot/patch.rs @@ -81,6 +81,23 @@ impl AppliedPatchSet { pub fn is_empty(&self) -> bool { self.removed.is_empty() && self.added.is_empty() && self.updated.is_empty() } + + /// Merge multiple patch sets into a single consolidated set. + /// + /// Different platforms deliver VFS events in different orders for the + /// same filesystem operation (e.g., macOS FSEvents sends CREATE first, + /// Windows ReadDirectoryChangesW sends REMOVE first for renames). + /// Merging ensures the WebSocket message structure is identical + /// regardless of event ordering. + pub fn merge(patches: Vec) -> AppliedPatchSet { + let mut merged = AppliedPatchSet::new(); + for patch in patches { + merged.removed.extend(patch.removed); + merged.added.extend(patch.added); + merged.updated.extend(patch.updated); + } + merged + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/tests/tests/connected_mode.rs b/tests/tests/connected_mode.rs index 27202d3b..a9392ba5 100644 --- a/tests/tests/connected_mode.rs +++ b/tests/tests/connected_mode.rs @@ -1479,6 +1479,10 @@ fn clean_to_slug_to_clean_via_filesystem() { let info = session.get_api_rojo().unwrap(); let root_id = info.root_instance_id; + // Intern the initial tree so parent Refs are in the redaction map + let read_response = session.get_api_read(root_id).unwrap(); + read_response.intern_and_redact(&mut redactions, root_id); + let src = session.path().join("src"); // Step 1: Add meta to Normal.luau -> name becomes "Nor:mal"