Skip to content

Fix: Commit lifetime effects from the final tree#61

Merged
phranck merged 5 commits into
mainfrom
issue/57-pending-effect-diff
Jul 21, 2026
Merged

Fix: Commit lifetime effects from the final tree#61
phranck merged 5 commits into
mainfrom
issue/57-pending-effect-diff

Conversation

@phranck

@phranck phranck commented Jul 21, 2026

Copy link
Copy Markdown
Owner

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

  • Add 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, and onAppear observes the written frame
  • Defer onChange actions and tracked-value writes to commit; per-pass index claims fix the first-frame double-fire
  • Rework onPreferenceChange to SwiftUI semantics (fires once per subtree-value change against the last committed frame); delete the dead PreferenceStorage callback machinery
  • Run state/cache/observation GC on the committed tree only; measure passes evaluate bodies plainly and register no observation callbacks
  • Remove the token-based LifecycleManager APIs and StateRegistration legacy; migrate Spinner (no more per-frame task restarts), Image (source as restart ID), and NotificationHost (expiry as restart ID, no resetAppearance workaround) to identity slots
  • Regenerate the API compatibility manifest with the tool

Test plan

  • ./scripts/test-linux.sh green 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 remains
  • New: PendingEffectCommitTests — superseded-pass effects, appear-after-writeFrame ordering, measure-pass observation, ghost-state GC, change-driven preference firing are hard expectations
  • scripts/verify-compatibility-manifest.sh byte-identical + valid

phranck added 5 commits July 21, 2026 22:36
- 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
Base automatically changed from issue/56-pass-collectors to main July 21, 2026 22:42
@phranck
phranck merged commit 618e0c3 into main Jul 21, 2026
@phranck
phranck deleted the issue/57-pending-effect-diff branch July 21, 2026 22:43
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.

[P0-08c] Commit lifetime effects from the final tree

1 participant