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
9 changes: 7 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@ MagnifierOverlay

```text
SettingsCoordinator.Current
(read-only snapshot contract)
↓
CreateWorkingCopy
↓
SettingsUseCaseContext
(shared snapshot/event/commit plumbing)
↓
Domain-service mutation
↓
SettingsNormalizer.Normalize
Expand All @@ -57,13 +61,14 @@ Current.ReplaceWith
one synchronous Changed event
```

A failed mutation or failed write does not replace committed settings and does not publish a settings change. `SettingsCoordinator.Current` returns a defensive snapshot, so consumers cannot mutate the committed in-memory object outside a transaction.
A failed mutation or failed write does not replace committed settings and does not publish a settings change. `SettingsCoordinator.Current` exposes `IReadOnlySightAdaptSettings` and returns a defensive snapshot. Mutable `SightAdaptSettings` is confined to explicit transaction callbacks and normalization/persistence boundaries. `SettingsUseCaseContext` centralizes snapshot, change-event and commit plumbing for application use-case façades without sharing their domain commands. `Commit` and `PersistCurrent` use one persistence execution path; only `Commit` publishes the synchronous `Changed` event after a successful write.

## Authorities

| Concern | Authority |
|---|---|
| Settings transaction and published snapshots | `SettingsCoordinator` |
| Settings transaction and published read-only snapshots | `SettingsCoordinator` |
| Shared use-case snapshot/event/commit plumbing | `SettingsUseCaseContext` |
| Settings JSON persistence and atomic replacement | `SettingsStore` |
| Persisted JSON DTOs and legacy-field migration | `PersistedSettingsMapper` |
| Schema, profile, assignment and reference normalization | explicit `ISettingsNormalizationPass` implementations |
Expand Down
36 changes: 22 additions & 14 deletions src/SightAdapt/ConfigurationUseCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,39 @@ namespace SightAdapt;

internal sealed class ConfigurationUseCases
{
private readonly SettingsCoordinator _settingsCoordinator;
private readonly SettingsUseCaseContext _settings;

public ConfigurationUseCases(
SettingsCoordinator settingsCoordinator)
{
_settingsCoordinator = settingsCoordinator ??
: this(new SettingsUseCaseContext(
settingsCoordinator ??
throw new ArgumentNullException(
nameof(settingsCoordinator));
nameof(settingsCoordinator))))
{
}

internal ConfigurationUseCases(
SettingsUseCaseContext settings)
{
_settings = settings ??
throw new ArgumentNullException(nameof(settings));
}

public SightAdaptSettings Snapshot =>
_settingsCoordinator.Current;
public IReadOnlySightAdaptSettings Snapshot =>
_settings.Snapshot;

public string SettingsPath =>
_settingsCoordinator.SettingsPath;
_settings.SettingsPath;

public event EventHandler? Changed
{
add => _settingsCoordinator.Changed += value;
remove => _settingsCoordinator.Changed -= value;
add => _settings.Changed += value;
remove => _settings.Changed -= value;
}

public SettingsCommitResult SetAutomaticMode(bool enabled)
{
return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
AutomaticModeManagementService.Set(
settings,
Expand All @@ -41,7 +49,7 @@ public SettingsCommitResult Apply(
ArgumentException.ThrowIfNullOrWhiteSpace(
change.ExecutablePath);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
var assignment =
ProfileResolver.RequireAssignmentByExecutablePath(
Expand Down Expand Up @@ -85,7 +93,7 @@ public SettingsCommitResult<bool> AddOrEnable(
{
ArgumentNullException.ThrowIfNull(identity);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
var result =
ApplicationAssignmentService.AddOrEnable(
Expand All @@ -100,7 +108,7 @@ public SettingsCommitResult Remove(string executablePath)
{
ArgumentException.ThrowIfNullOrWhiteSpace(executablePath);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
ApplicationAssignmentService.Remove(
settings,
ProfileResolver.RequireAssignmentByExecutablePath(
Expand All @@ -115,7 +123,7 @@ public SettingsCommitResult UpdateTuning(
ArgumentException.ThrowIfNullOrWhiteSpace(profileId);
ArgumentNullException.ThrowIfNull(values);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
VisualProfileManagementService.UpdateTuning(
settings,
ProfileResolver.RequireVisualProfile(
Expand Down
6 changes: 3 additions & 3 deletions src/SightAdapt/RuntimeCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ internal sealed class RuntimeCoordinator
private readonly IRuntimeEnvironment _environment;
private readonly RuntimeOverlayActivator _overlayActivator;
private readonly AutomaticActivationService _automaticActivation;
private readonly Func<SightAdaptSettings> _readSettings;
private readonly Func<IReadOnlySightAdaptSettings> _readSettings;
private bool _committingSettings;

public RuntimeCoordinator(
Expand Down Expand Up @@ -43,7 +43,7 @@ internal RuntimeCoordinator(
ApplicationStateController stateController,
IRuntimeOverlay overlay,
IRuntimeEnvironment environment,
Func<SightAdaptSettings>? readSettings)
Func<IReadOnlySightAdaptSettings>? readSettings)
{
_settingsCoordinator = settingsCoordinator ??
throw new ArgumentNullException(nameof(settingsCoordinator));
Expand Down Expand Up @@ -257,7 +257,7 @@ public void DisableForExit()
_overlayActivator.DisableForExit();
}

private SightAdaptSettings ReadSettings()
private IReadOnlySightAdaptSettings ReadSettings()
{
return _readSettings();
}
Expand Down
53 changes: 28 additions & 25 deletions src/SightAdapt/SettingsCoordinator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public SettingsCoordinator(
_current = _store.Load();
}

public SightAdaptSettings Current =>
public IReadOnlySightAdaptSettings Current =>
_current.CreateWorkingCopy();

public string SettingsPath => _store.SettingsPath;
Expand Down Expand Up @@ -98,13 +98,36 @@ public SettingsCommitResult<T> Commit<T>(
Func<SightAdaptSettings, T> mutation)
{
ArgumentNullException.ThrowIfNull(mutation);
return ExecuteTransaction(
mutation,
publishChanged: true);
}

public SettingsCommitResult PersistCurrent()
{
var result = ExecuteTransaction<object?>(
_ => null,
publishChanged: false);

return result.Succeeded
? SettingsCommitResult.Success()
: SettingsCommitResult.Failure(
result.ErrorMessage ??
"Settings could not be saved.");
}

private SettingsCommitResult<T> ExecuteTransaction<T>(
Func<SightAdaptSettings, T> operation,
bool publishChanged)
{
ArgumentNullException.ThrowIfNull(operation);

var candidate = _current.CreateWorkingCopy();
T value;

try
{
value = mutation(candidate);
value = operation(candidate);
_store.Save(candidate);
}
catch (Exception exception)
Expand All @@ -120,32 +143,12 @@ public SettingsCommitResult<T> Commit<T>(
}

_current.ReplaceWith(candidate);
Changed?.Invoke(this, EventArgs.Empty);
return SettingsCommitResult<T>.Success(value);
}

public SettingsCommitResult PersistCurrent()
{
var candidate = _current.CreateWorkingCopy();

try
{
_store.Save(candidate);
}
catch (Exception exception)
when (IsExpectedError(exception))
if (publishChanged)
{
return SettingsCommitResult.Failure(
FormatError(exception));
}
catch (Exception exception)
{
_reportUnexpectedError(exception);
throw;
Changed?.Invoke(this, EventArgs.Empty);
}

_current.ReplaceWith(candidate);
return SettingsCommitResult.Success();
return SettingsCommitResult<T>.Success(value);
}

private static bool IsExpectedError(
Expand Down
33 changes: 33 additions & 0 deletions src/SightAdapt/SettingsUseCaseContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace SightAdapt;

internal sealed class SettingsUseCaseContext
{
private readonly SettingsCoordinator _coordinator;

public SettingsUseCaseContext(
SettingsCoordinator coordinator)
{
_coordinator = coordinator ??
throw new ArgumentNullException(nameof(coordinator));
}

public IReadOnlySightAdaptSettings Snapshot =>
_coordinator.Current;

public string SettingsPath =>
_coordinator.SettingsPath;

public event EventHandler? Changed
{
add => _coordinator.Changed += value;
remove => _coordinator.Changed -= value;
}

public SettingsCommitResult Commit(
Action<SightAdaptSettings> mutation) =>
_coordinator.Commit(mutation);

public SettingsCommitResult<T> Commit<T>(
Func<SightAdaptSettings, T> mutation) =>
_coordinator.Commit(mutation);
}
34 changes: 21 additions & 13 deletions src/SightAdapt/VisualProfileUseCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,31 @@ namespace SightAdapt;

internal sealed class VisualProfileUseCases
{
private readonly SettingsCoordinator _settingsCoordinator;
private readonly SettingsUseCaseContext _settings;

public VisualProfileUseCases(
SettingsCoordinator settingsCoordinator)
{
_settingsCoordinator = settingsCoordinator ??
: this(new SettingsUseCaseContext(
settingsCoordinator ??
throw new ArgumentNullException(
nameof(settingsCoordinator));
nameof(settingsCoordinator))))
{
}

internal VisualProfileUseCases(
SettingsUseCaseContext settings)
{
_settings = settings ??
throw new ArgumentNullException(nameof(settings));
}

public SightAdaptSettings Snapshot =>
_settingsCoordinator.Current;
public IReadOnlySightAdaptSettings Snapshot =>
_settings.Snapshot;

public event EventHandler? Changed
{
add => _settingsCoordinator.Changed += value;
remove => _settingsCoordinator.Changed -= value;
add => _settings.Changed += value;
remove => _settings.Changed -= value;
}

public string CreateAvailableName(string baseName)
Expand All @@ -31,7 +39,7 @@ public string CreateAvailableName(string baseName)

public SettingsCommitResult<string> Create(string name)
{
return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
VisualProfileManagementService.Create(
settings,
name).Id);
Expand All @@ -43,7 +51,7 @@ public SettingsCommitResult<string> Duplicate(
{
ArgumentException.ThrowIfNullOrWhiteSpace(sourceProfileId);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
VisualProfileManagementService.Duplicate(
settings,
ProfileResolver.RequireVisualProfile(
Expand All @@ -58,7 +66,7 @@ public SettingsCommitResult<string> Rename(
{
ArgumentException.ThrowIfNullOrWhiteSpace(profileId);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
VisualProfileManagementService.Rename(
settings,
Expand All @@ -77,7 +85,7 @@ public SettingsCommitResult<string> UpdateTuning(
ArgumentException.ThrowIfNullOrWhiteSpace(profileId);
ArgumentNullException.ThrowIfNull(values);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
VisualProfileManagementService.UpdateTuning(
settings,
Expand All @@ -97,7 +105,7 @@ public SettingsCommitResult<string> Delete(
ArgumentException.ThrowIfNullOrWhiteSpace(
fallbackProfileId);

return _settingsCoordinator.Commit(settings =>
return _settings.Commit(settings =>
{
VisualProfileManagementService.Delete(
settings,
Expand Down
Loading
Loading