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(); + } } } }