Fix: Serialize AppStorage persistence and inject its backend per app - #65
Merged
Conversation
- Capture an immutable cache snapshot under the lock for every mutation and persist snapshots on one serial writer queue, so an older snapshot can never overwrite a newer mutation - Make synchronize() a true barrier that waits for every previously enqueued write before returning - Keep atomic file replacement and complete pending writes even if the storage instance is released - Add ordering, barrier, restart, corruption, and concurrency stress tests against an injectable persistence step
- Add StoragePersistenceError with sanitized reasons (error domain and code only, never paths or stored content) - Route encode, serialize, and write failures through an injectable handler; the default logs one line to standard error - Cover failing writes, unwritable destinations, and encoding failures with tests asserting the storage path never leaks
- Delete the deprecated mutable StorageDefaults global - Give every production runtime its own JSONFileStorage instance so two runtimes never share a backend unless explicitly injected - Route AppStorage access outside a runtime to a deliberately volatile process-local fallback that never touches the file system
- Drop policy overrides for the removed StorageDefaults global and the replaced JSONFileStorage initializers - Add tuiSpecific overrides for StoragePersistenceError and the new failure-reporting initializers, copied from fresh 6.0.3 snapshots - Regenerate the manifest with TUIkitAPICheck against the assembled macOS and Linux snapshot set
- Document ordered snapshot writes, the synchronize barrier, and sanitized failure reporting in the state management article - Document the volatile fallback for properties accessed outside a runtime
wadetregaskis
added a commit
to wadetregaskis/TUIkit
that referenced
this pull request
Sep 2, 2026
The tail — storage, view contracts, data flow, scenes — judged at branch granularity, since by this point each branch's character was clear from its first commit and per-commit detail would not have changed a verdict. PR phranck#65's serialised writes are already ours (NSLock plus a serial save queue). Its error reporting is not: we drop persistence failures on the floor, so a read-only config directory looks like a successful save. Deferred — real, small, unforced. PRs phranck#67, phranck#69 and phranck#70 are deferred as a batch and explicitly NOT as upstream-review work. They are all one thing — align the framework's shape with SwiftUI's — and CLAUDE.md requires that to be answered against the real SDK, which is how SwiftUI-compatibility.md was built. Reading what upstream happened to write is the wrong instrument. Some of it is already shipped here; DynamicProperty as a public protocol is not. The next step is an audit pass over the data-flow and view-modifier surfaces with these branches as one input among several, which is a piece of work rather than triage. Also fixes the ledger's row order: recording 8a73ec3 out of sequence made `verify` report a gap, which is the check doing its job. Final spread across 205 commits: 56 n-a, 37 noise, 35 have, 35 defer, 17 adapt, 16 reject, 5 queued, 4 inspired. Co-Authored-By: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #15.
Summary
JSONFileStoragecaptures an immutable cache snapshot for every mutation and persists snapshots through one serial writer queue, so an older snapshot can never overwrite a newer mutation; pending writes complete even if the storage instance is releasedsynchronize()is now a true barrier that returns only after every previously issued write has completed; the runtime flushes storage this way during cleanupStoragePersistenceErrorwith sanitized reasons (error domain and code only, never paths or stored content); the default handler logs one line to stderrStorageDefaultsglobal is gone: every production runtime owns its ownJSONFileStorage, and@AppStorageaccess outside a runtime falls back to a deliberately volatile process-local backend that never touches the file systemAcceptance criteria from #15
synchronize()is a true barrier for all prior changes (barrier test)./scripts/test-linux.shgreen)Test plan
AppStoragePersistenceTestssuite: ordering, barrier, restart, corruption, failure reporting, unbound fallback, concurrency stressverify-compatibility-manifest.shpasses against the regenerated manifest