Fix: Commit lifetime effects from the final tree#61
Merged
Conversation
- Pin lifetime-effect leaks: superseded passes fire onAppear and mount tasks, onAppear runs before writeFrame, measure-pass observation trackings survive, GC keeps ghost state from discarded passes - Guard the stable case: a subtree present in every pass appears exactly once across a correction frame - Extract shared frame fixtures (GrowableHeaderModel, GrowingHeader, HeightGate) into test support - Mark desired behavior withKnownIssue until the pending diff lands (#57)
- Add PendingFrameEffects: an ordered per-pass command log for lifetime effects plus GC liveness sets, replayed once at frame commit after terminal output - Record onAppear/onDisappear/task in the pending log during RenderLoop passes; the live path (ViewRenderer, harnesses) keeps immediate semantics - Replay only the final pass's records — a task from a discarded pass is never even started, and onAppear observes the written frame - Promote the superseded-pass and appear-timing tests to hard expectations
- Record onChange actions and tracked-value writes in the pending log; per-pass index claims keep tracked values stable across correction passes - Rework onPreferenceChange to SwiftUI semantics: the action fires when the subtree's reduced value changes against the last committed frame (once on first appearance), at frame commit - Delete the now-unused PreferenceStorage callback machinery (no deprecations) - Promote the onChange characterization to a hard expectation; specify change-driven preference firing across correction frames
- Route all GC liveness (state, cache, observation) through the per-pass pending sets; the frame commit applies only the final pass's sets before the end-of-frame sweeps - Decouple observation liveness from track(): a registration made by a discarded pass is collected and its callback turns inert - Evaluate bodies plainly in the measure phase — sizing never registers invalidation callbacks - Promote the measure-pass observation and ghost-state tests to hard expectations; adapt registry unit tests to explicit liveness
- Delete the token-based LifecycleManager APIs; identity slots are the only lifecycle keying - Migrate Spinner, _ImageCore, and NotificationHost to identity slots with updateTask restart IDs (spinner no longer restarts its task every frame; the notification host drops its resetAppearance workaround; the image source doubles as the restart ID) - Route their state liveness and task mounts through the pending-effects commit - Remove the StateRegistration activeContext/counter/activeEnvironment fallbacks; TaskLocal scoping is the only hydration mechanism - Scope PreferenceStorage.adopt(from:) to package visibility - Migrate lifecycle, context, and environment tests to the identity APIs and TaskLocal scoping - Update review-policy overrides and regenerate the API compatibility manifest with the tool
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 #57 (sub-issue of #13, stacked on #60 — retarget to main after #60 merges). Design spec:
.claude/openspec/render-phase-effect-commit.md.Summary
PendingFrameEffects: an ordered per-pass command log for lifetime effects plus GC liveness sets; the frame commit replays only the FINAL pass's records after terminal output — a task from a discarded pass is never even started, andonAppearobserves the written frameonChangeactions and tracked-value writes to commit; per-pass index claims fix the first-frame double-fireonPreferenceChangeto SwiftUI semantics (fires once per subtree-value change against the last committed frame); delete the deadPreferenceStoragecallback machineryLifecycleManagerAPIs andStateRegistrationlegacy; migrate Spinner (no more per-frame task restarts), Image (source as restart ID), and NotificationHost (expiry as restart ID, noresetAppearanceworkaround) to identity slotsTest plan
./scripts/test-linux.shgreen on macOS (native, Swift 6.0.3) and Linux (Docker) — 1312 tests, only the pre-existing [P0-09] Preserve subtree liveness across EquatableView cache hits #14 marker remainsPendingEffectCommitTests— superseded-pass effects, appear-after-writeFrame ordering, measure-pass observation, ghost-state GC, change-driven preference firing are hard expectationsscripts/verify-compatibility-manifest.shbyte-identical + valid