diff --git a/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs b/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
index 66cd088..dc854ae 100644
--- a/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
+++ b/Vaults/Nodsoft.MoltenObsidian.Vaults.FileSystem/FileSystemVault.cs
@@ -51,8 +51,18 @@ public sealed class FileSystemVault : IWritableVault
///
public event IVault.VaultUpdateEventHandler? VaultUpdate;
-
+ ///
+ /// Provides a dictionary of file change locks, by last datetime modified.
+ /// This is used to mitigate duplicate dispatches of a given file change event.
+ ///
+ private readonly ConcurrentDictionary _fileChangeLocks = [];
+
+ ///
+ /// Debounce period between file changes.
+ ///
+ private static readonly TimeSpan DebouncePeriod = TimeSpan.FromMilliseconds(100);
+
///
/// Gets the default list of folders to ignore when loading a vault.
///
@@ -228,7 +238,32 @@ private async ValueTask OnItemChangedAsync(object sender, FileSystemEventArgs e)
{
if (_files.TryGetValue(relativePath, out IVaultFile? file))
{
- await (VaultUpdate?.Invoke(this, new(UpdateType.Update, file)) ?? new());
+ // Ensure hashes differ
+ (SemaphoreSlim semaphore, _) = _fileChangeLocks.GetOrAdd(relativePath, _ => (new(1, 1), DateTime.UnixEpoch));
+
+ if (semaphore.CurrentCount is 0)
+ {
+ return;
+ }
+
+ await semaphore.WaitAsync();
+
+ try
+ {
+ // Update w/ reference hash at time of lock
+ (_, DateTime lastChange) = _fileChangeLocks[relativePath];
+ DateTime current = DateTime.UtcNow;
+
+ if (lastChange.Add(DebouncePeriod) < current)
+ {
+ await (VaultUpdate?.Invoke(this, new(UpdateType.Update, file)) ?? new());
+ _fileChangeLocks[relativePath] = new(semaphore, current);
+ }
+ }
+ finally
+ {
+ semaphore.Release();
+ }
}
}
}