Skip to content

feat: WritableStore PatchAsync (atomic batch writes)#50

Merged
windischb merged 1 commit into
developfrom
feature/writable-store-patch
Jun 1, 2026
Merged

feat: WritableStore PatchAsync (atomic batch writes)#50
windischb merged 1 commit into
developfrom
feature/writable-store-patch

Conversation

@windischb

Copy link
Copy Markdown
Contributor

Summary

Adds PatchAsync to IWritableStore<T> — the one new feature: atomic batch overlay writes. Any number of Set/SetSecret/Reset mutations apply under a single read-merge-write — one backend write, one recompute — instead of one per property. A 20-field form save no longer fires 20 recomputes, subscribers never observe a half-applied state, and for a DB-backed IStoreBackend it collapses N round-trips into one.

await store.PatchAsync(b => b
    .Set(x => x.Host, "smtp.example.com")
    .Set(x => x.Port, 587)
    .SetSecret(x => x.ApiKey, envelope)   // distinct method, mirrors SetSecretAsync
    .Reset(x => x.Timeout));              // mix sets and resets

API (new)

  • IWritableStore<T>.PatchAsync(Action<IWritableStorePatch<T>>, ct) + async Func<.., Task> overload (closes the async void footgun, allows awaiting secret encryption inline).
  • IWritableStorePatch<T> builder: Set (value incl. explicit null), SetSecret (pre-encrypted envelope), Reset. Presence-based semantics — present = set, absent = untouched, Reset = remove the override (the only model that allows setting null explicitly and deleting an override).
  • Single-value SetAsync/SetSecretAsync/ResetAsync now delegate to PatchAsync.

Additive for callers (resolved via DI). → minor bump (5.2).

Fixed

  • Resetting a secret-typed member no longer throws NotSupportedException (removing an override exposes no plaintext) — symmetry with SetSecretAsync.

Internal cleanup (no public-API impact)

  • Pipeline layer merging is now case-insensitive on property names (Cocoar.Json.Mutable 1.2.0), consistent with how the effective config is read back (STJ case-insensitive, like IConfiguration).
  • This removes the overlay's write-time base-casing alignment ("Trap B"): SparseOverlayMutator is reduced to thin MutableJsonPath glue (baseDom gone), and PatchAsync parses/serializes the overlay once per batch instead of once per property.

Docs & tests

  • website/guide/providers/writable-store.md (Batch-writes section, presence semantics, secrets correction, form-save endpoint), changelog.md ([Unreleased]), WritableStoreExample.
  • All test projects green (incl. 216 Core tests exercising the merge, and the Trap-B end-to-end tests).

Deliberately not in scope: history/rollback, WithReason, and a generic JSON/HTTP ApplyJson path — those are app policy; the consumer owns the HTTP boundary with typed DTOs.

🤖 Generated with Claude Code

…ve layer merge

Add PatchAsync to IWritableStore<T> for atomic batch overlay writes: any
number of Set/SetSecret/Reset mutations apply under one read-merge-write —
one backend write, one recompute — instead of one per property. A 20-field
form save no longer fires 20 recomputes, and subscribers never observe a
half-applied state. For a DB-backed IStoreBackend this collapses N round-trips
into one.

- IWritableStorePatch<T> builder: Set (value, incl. explicit null),
  SetSecret (pre-encrypted envelope), Reset. Presence-based semantics —
  present = set, absent = untouched, Reset = remove the override.
- Sync + async (Func<.., Task>) overloads; the async one closes the
  async-void footgun and allows awaiting (e.g. secret encryption) inline.
- Single-value SetAsync/SetSecretAsync/ResetAsync now delegate to PatchAsync.

Fix: resetting a secret-typed member no longer throws (removing an override
exposes no plaintext) — symmetry with SetSecretAsync.

Internal cleanup (no public-API impact): pipeline layer merging is now
case-insensitive on property names (Cocoar.Json.Mutable 1.2.0), consistent
with how the effective config is read back (System.Text.Json case-insensitive,
like IConfiguration). This removes the overlay's write-time base-casing
alignment (Trap B): SparseOverlayMutator is reduced to thin MutableJsonPath
glue (baseDom gone), and PatchAsync parses/serializes the overlay once per
batch instead of once per property.

Docs, changelog ([Unreleased]) and the WritableStoreExample updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@windischb windischb merged commit b0ce56e into develop Jun 1, 2026
7 of 8 checks passed
@windischb windischb deleted the feature/writable-store-patch branch June 1, 2026 07:20
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