Skip to content

refactor(config): move grid-style Save off the UI thread - #176

Merged
mrcsin merged 1 commit into
masterfrom
grid-style-async-save
Aug 5, 2026
Merged

mrcsin merged 1 commit into
masterfrom
grid-style-async-save

Conversation

@mrcsin

@mrcsin mrcsin commented Aug 5, 2026

Copy link
Copy Markdown
Member

Problem

The grid-style editor's Load is async, but Save was synchronous — SaveCommand = ReactiveCommand.Create(Save, …) did file I/O (File.WriteAllText + File.Move) on the UI thread, blocking the dispatcher on every save. Slice 2 of the #118 grid-style debloat.

What changed

Make the write path async end to end, matching Load:

  • IGridStyleEditorFacade.Save → Task<Result>; GridStyleEditorFacade.Save awaits GridStyleWriter.SaveAsync.
  • GridStyleWriter — the temp-file write and header read use WriteAllTextAsync / ReadAllLinesAsync; File.Move stays synchronous inside the atomic-write try/catch (there is no async move, and it's a fast metadata rename). The slice-7 Rule-B GridStyleSaveFailedError.CausedBy(ex) envelope is preserved; the private helpers gained the Async suffix (WriteAtomicAsync/ReadLeadingCommentBlockAsync).
  • GridStyleEditorViewModel — Save → SaveAsync, wired via ReactiveCommand.CreateFromTask. SaveCommand stays ReactiveCommand<Unit, bool>, so the window's save→restart-prompt→close flow and the ThrownExceptions → ReportSaveException routing are unchanged (GridStyleEditorWindow.axaml.cs is untouched).

Behavior-preserving: the bytes written are byte-identical — only the write's threading changes. Validate stays synchronous (it runs per-keystroke in RecomputeCanSave), no CancellationToken is threaded (Save is a button-click), and slice 1's round-trip guards still hold. Nothing from slices 3–5 (record nesting, StyleColor, the VM split) leaks in.

Verification

  • dotnet build SemiStep.slnx — 0 warnings; dotnet test — 1687 passed, 0 failed; dotnet format clean.
  • Slice-1 guard SaveThenLoad_DistinctFixture… (now awaiting the async Save) is green — proving identical bytes. The atomic-write / File.Move-failure test and the save-fault tests (ThrowingFacade's synchronous throw → ReportSaveException; CausedByFailingFacade → LogCausedByExceptions) still fire after the conversion.
  • Review chain: comprehensive (5 agents, all OUTCOME ACHIEVED) → smells → comment audit → critical, all clean after one fix (the async-helper suffix rename). A second finding — the facade's Save lacking the Async suffix — was correctly kept: the facade's Load is also suffix-less-async, so Save matching it is the consistent choice.
  • Manual test (operator): confirmed.

Slice 2 of #118 (stays open). Next: slice 3 nests GridStyleOptions to mirror the DTO groups — the big line-count win.

Per-task commits before collapse
0b6b170 docs(plan): record exec branch and verify steps for async save
33adcad refactor(config): suffix grid-style writer async helpers
61d3aa2 docs(config): note grid-style Save is now async
06c4f72 refactor(config): make grid-style Save async off the UI thread

🤖 Generated with Claude Code

The style editor's Load was async but Save was synchronous, so
`SaveCommand = ReactiveCommand.Create(Save, …)` blocked the dispatcher
on the file write. Make the write path async end to end, matching Load
(slice 2 of the #118 grid-style debloat).

- IGridStyleEditorFacade.Save returns Task<Result>; the facade awaits
  GridStyleWriter.SaveAsync.
- The writer's temp-file write and header read use WriteAllTextAsync /
  ReadAllLinesAsync; File.Move stays synchronous inside the atomic-write
  try/catch (no async move exists, and the rename is a fast metadata op).
  The Rule-B GridStyleSaveFailedError.CausedBy(ex) envelope is preserved.
- The view model's Save becomes SaveAsync, wired via
  ReactiveCommand.CreateFromTask; SaveCommand stays ReactiveCommand<Unit,
  bool>, so the window's save->restart-prompt->close flow and the
  ThrownExceptions -> ReportSaveException routing are unchanged.

Behavior-preserving: the bytes written are identical, only the write's
threading changes. Validate stays synchronous (it runs per keystroke in
RecomputeCanSave), and no CancellationToken is threaded (Save is a
button-click). Slice 1's round-trip guards still hold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mrcsin
mrcsin marked this pull request as ready for review August 5, 2026 13:37
@mrcsin
mrcsin merged commit f6fc9a4 into master Aug 5, 2026
1 check passed
@mrcsin
mrcsin deleted the grid-style-async-save branch August 5, 2026 13:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant