Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,12 @@ SharpSync is a **pure .NET file synchronization library** with no native depende

### Important Considerations

1. **Thread Safety**: `SyncEngine` instances are NOT thread-safe. Use one per thread.
1. **Threading Model**: Only one sync operation can run at a time per `SyncEngine` instance. However, the following are thread-safe and can be called from any thread (including while sync runs):
- State properties: `IsSynchronizing`, `IsPaused`, `State`
- Change notifications: `NotifyLocalChangeAsync`, `NotifyLocalChangesAsync`, `NotifyLocalRenameAsync`
- Control methods: `PauseAsync`, `ResumeAsync`
- Query methods: `GetPendingOperationsAsync`, `GetRecentOperationsAsync`
- `ClearPendingChanges`
2. **No UI Dependencies**: Library is UI-agnostic, suitable for any .NET application
3. **Conflict Resolution**: Provides data for UI decisions without implementing UI
4. **OAuth2 Flow**: Caller must implement browser-based auth flow
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,16 @@ SharpSync uses a modular, interface-based architecture:

### Thread Safety

`SyncEngine` instances are **not thread-safe**. Use one instance per sync operation. You can safely run multiple sync operations in parallel using separate `SyncEngine` instances.
Only one sync operation can run at a time per `SyncEngine` instance. However, the following members are **thread-safe** and can be called from any thread (including while a sync runs):

- **State properties**: `IsSynchronizing`, `IsPaused`, `State`
- **Change notifications**: `NotifyLocalChangeAsync()`, `NotifyLocalChangesAsync()`, `NotifyLocalRenameAsync()` - safe to call from FileSystemWatcher threads
- **Control methods**: `PauseAsync()`, `ResumeAsync()` - safe to call from UI thread
- **Query methods**: `GetPendingOperationsAsync()`, `GetRecentOperationsAsync()`, `ClearPendingChanges()`

This design supports typical desktop client integration where FileSystemWatcher events arrive on thread pool threads, sync runs on a background thread, and UI controls pause/resume from the main thread.

You can safely run multiple sync operations in parallel using **separate** `SyncEngine` instances.

## Requirements

Expand Down
61 changes: 55 additions & 6 deletions src/SharpSync/Core/ISyncEngine.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
namespace Oire.SharpSync.Core;

/// <summary>
/// Interface for the sync engine that orchestrates synchronization between storages
/// Interface for the sync engine that orchestrates synchronization between storages.
/// </summary>
/// <remarks>
/// <para><b>Threading Model:</b></para>
/// <para>
/// Only one sync operation (<see cref="SynchronizeAsync"/>, <see cref="SyncFolderAsync"/>,
/// or <see cref="SyncFilesAsync"/>) can run at a time. Attempting to start a concurrent sync
/// throws <see cref="InvalidOperationException"/>.
/// </para>
/// <para><b>Thread-Safe Members:</b></para>
/// <list type="bullet">
/// <item><description><see cref="IsSynchronizing"/>, <see cref="IsPaused"/>, <see cref="State"/> - Safe to read from any thread</description></item>
/// <item><description><see cref="NotifyLocalChangeAsync"/>, <see cref="NotifyLocalChangesAsync"/>, <see cref="NotifyLocalRenameAsync"/> - Safe to call from FileSystemWatcher threads</description></item>
/// <item><description><see cref="PauseAsync"/>, <see cref="ResumeAsync"/> - Safe to call from UI thread while sync runs</description></item>
/// <item><description><see cref="GetPendingOperationsAsync"/>, <see cref="GetRecentOperationsAsync"/> - Safe to call while sync runs</description></item>
/// <item><description><see cref="ClearPendingChanges"/> - Safe to call from any thread</description></item>
/// </list>
/// <para>
/// This design supports typical desktop client integration where FileSystemWatcher events
/// arrive on thread pool threads, sync runs on a background thread, and UI controls
/// pause/resume from the main thread.
/// </para>
/// </remarks>
public interface ISyncEngine: IDisposable {
/// <summary>
/// Event raised to report synchronization progress
Expand All @@ -15,18 +36,30 @@ public interface ISyncEngine: IDisposable {
event EventHandler<FileConflictEventArgs>? ConflictDetected;

/// <summary>
/// Gets whether the engine is currently synchronizing
/// Gets whether the engine is currently synchronizing.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// Returns <c>true</c> when a sync operation is in progress, including when paused.
/// </remarks>
bool IsSynchronizing { get; }

/// <summary>
/// Gets whether the engine is currently paused
/// Gets whether the engine is currently paused.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// </remarks>
bool IsPaused { get; }

/// <summary>
/// Gets the current state of the sync engine
/// Gets the current state of the sync engine.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// Possible values are <see cref="SyncEngineState.Idle"/>,
/// <see cref="SyncEngineState.Running"/>, and <see cref="SyncEngineState.Paused"/>.
/// </remarks>
SyncEngineState State { get; }

/// <summary>
Expand Down Expand Up @@ -70,9 +103,11 @@ public interface ISyncEngine: IDisposable {
Task ResetSyncStateAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Pauses the current synchronization operation
/// Pauses the current synchronization operation.
/// </summary>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including the UI thread while a sync operation runs on a background thread.</para>
/// <para>
/// The pause is graceful - the engine will complete the current file operation
/// before entering the paused state. This ensures no partial file transfers occur.
Expand All @@ -89,9 +124,11 @@ public interface ISyncEngine: IDisposable {
Task PauseAsync();

/// <summary>
/// Resumes a paused synchronization operation
/// Resumes a paused synchronization operation.
/// </summary>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including the UI thread while a sync operation is paused.</para>
/// <para>
/// If the engine is not paused, this method returns immediately.
/// </para>
Expand Down Expand Up @@ -162,6 +199,9 @@ public interface ISyncEngine: IDisposable {
/// <param name="changeType">The type of change that occurred</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation</param>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including FileSystemWatcher event handlers which run on thread pool threads. It can be
/// called concurrently with running sync operations.</para>
/// <para>
/// This method allows desktop clients to feed FileSystemWatcher events directly to the
/// sync engine for efficient incremental change detection, avoiding the need for full scans.
Expand Down Expand Up @@ -195,6 +235,8 @@ public interface ISyncEngine: IDisposable {
/// <param name="changes">Collection of path and change type pairs</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation</param>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including FileSystemWatcher event handlers. It can be called concurrently with running sync operations.</para>
/// <para>
/// This method is more efficient than calling <see cref="NotifyLocalChangeAsync"/> multiple times
/// when handling bursts of FileSystemWatcher events. Changes are coalesced internally.
Expand All @@ -218,6 +260,8 @@ public interface ISyncEngine: IDisposable {
/// <param name="newPath">The new relative path after the rename</param>
/// <param name="cancellationToken">Cancellation token to cancel the operation</param>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including FileSystemWatcher Renamed event handlers. It can be called concurrently with running sync operations.</para>
/// <para>
/// This method properly tracks rename operations by recording both the deletion of the
/// old path and the creation of the new path. This allows the sync engine to optimize
Expand All @@ -243,6 +287,8 @@ public interface ISyncEngine: IDisposable {
/// <param name="cancellationToken">Cancellation token to cancel the operation</param>
/// <returns>A collection of pending sync operations</returns>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including while a sync operation is running. It returns a snapshot of the current pending state.</para>
/// <para>
/// This method returns the current queue of pending operations based on tracked changes.
/// Desktop clients can use this to:
Expand All @@ -266,6 +312,7 @@ public interface ISyncEngine: IDisposable {
/// <see cref="NotifyLocalChangesAsync"/>, or <see cref="NotifyLocalRenameAsync"/>.
/// </summary>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread.</para>
/// <para>
/// Use this method to discard pending notifications without performing synchronization.
/// This is useful when:
Expand All @@ -290,6 +337,8 @@ public interface ISyncEngine: IDisposable {
/// <param name="cancellationToken">Cancellation token to cancel the operation</param>
/// <returns>A collection of completed operations ordered by completion time descending</returns>
/// <remarks>
/// <para><b>Thread Safety:</b> This method is thread-safe and can be called from any thread,
/// including while a sync operation is running.</para>
/// <para>
/// Desktop clients can use this method to:
/// <list type="bullet">
Expand Down
44 changes: 39 additions & 5 deletions src/SharpSync/Sync/SyncEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,30 @@
namespace Oire.SharpSync.Sync;

/// <summary>
/// Sync engine with incremental sync, change detection, and parallel processing
/// Optimized for large file sets and efficient synchronization
/// Sync engine with incremental sync, change detection, and parallel processing.
/// Optimized for large file sets and efficient synchronization.
/// </summary>
/// <remarks>
/// <para><b>Threading Model:</b></para>
/// <para>
/// Only one sync operation (<see cref="SynchronizeAsync"/>, <see cref="SyncFolderAsync"/>,
/// or <see cref="SyncFilesAsync"/>) can run at a time. Attempting to start a concurrent sync
/// throws <see cref="InvalidOperationException"/>.
/// </para>
/// <para><b>Thread-Safe Members:</b></para>
/// <list type="bullet">
/// <item><description><see cref="IsSynchronizing"/>, <see cref="IsPaused"/>, <see cref="State"/> - Safe to read from any thread</description></item>
/// <item><description><see cref="NotifyLocalChangeAsync"/>, <see cref="NotifyLocalChangesAsync"/>, <see cref="NotifyLocalRenameAsync"/> - Safe to call from FileSystemWatcher threads</description></item>
/// <item><description><see cref="PauseAsync"/>, <see cref="ResumeAsync"/> - Safe to call from UI thread while sync runs</description></item>
/// <item><description><see cref="GetPendingOperationsAsync"/>, <see cref="GetRecentOperationsAsync"/> - Safe to call while sync runs</description></item>
/// <item><description><see cref="ClearPendingChanges"/> - Safe to call from any thread</description></item>
/// </list>
/// <para>
/// This design supports typical desktop client integration where FileSystemWatcher events
/// arrive on thread pool threads, sync runs on a background thread, and UI controls
/// pause/resume from the main thread.
/// </para>
/// </remarks>
public class SyncEngine: ISyncEngine {
private readonly ISyncStorage _localStorage;
private readonly ISyncStorage _remoteStorage;
Expand Down Expand Up @@ -43,18 +64,31 @@ public class SyncEngine: ISyncEngine {
private readonly ConcurrentDictionary<string, PendingChange> _pendingChanges = new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// Gets whether the engine is currently synchronizing
/// Gets whether the engine is currently synchronizing.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// It returns <c>true</c> when a sync operation is in progress,
/// including when paused.
/// </remarks>
public bool IsSynchronizing => _syncSemaphore.CurrentCount == 0;

/// <summary>
/// Gets whether the engine is currently paused
/// Gets whether the engine is currently paused.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// </remarks>
public bool IsPaused => _state == SyncEngineState.Paused;

/// <summary>
/// Gets the current state of the sync engine
/// Gets the current state of the sync engine.
/// </summary>
/// <remarks>
/// This property is thread-safe and can be read from any thread.
/// Possible values are <see cref="SyncEngineState.Idle"/>,
/// <see cref="SyncEngineState.Running"/>, and <see cref="SyncEngineState.Paused"/>.
/// </remarks>
public SyncEngineState State => _state;

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions tests/SharpSync.Tests/Storage/WebDavStorageTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ public async Task ListItemsAsync_WithFiles_ReturnsAllItems() {
}

// Assert
Assert.NotNull(items);
Assert.Equal(3, items.Count);
Assert.Contains(items, i => i.Path.EndsWith("file1.txt") && !i.IsDirectory);
Assert.Contains(items, i => i.Path.EndsWith("file2.txt") && !i.IsDirectory);
Expand Down
Loading