{{ message }}
fix(core): wait for app stability before cleaning up dehydrated views - #70575
Draft
arturovt wants to merge 1 commit into
Draft
fix(core): wait for app stability before cleaning up dehydrated views#70575arturovt wants to merge 1 commit into
arturovt wants to merge 1 commit into
Conversation
This fixes a production crash seen on pages that dynamically create
non-defer components:
TypeError: Cannot set properties of null (setting '__ngContext__')
...
at locateOrCreateElementNodeImpl
The crash was traced to `triggerHydrationForBlockQueue()`. When a
`@defer` block finishes hydrating, it removes its own `PendingTasks`
entry and immediately calls `cleanupHydratedDeferBlocks()` ->
`cleanupDehydratedViews()`. That cleanup walks all LViews registered
with `ApplicationRef` and removes dehydrated views that aren't
associated with a defer block. This isn't safe while other async
hydration work is still pending.
For example, a dynamically created component can have its own
`PendingTasks` entry while its `ViewContainerRef.createComponent()`
call is still waiting to run. If an unrelated defer block finishes
first, its cleanup can remove that component's dehydrated view before
the component gets a chance to claim it. The later creation then sees
stale hydration state and can fail with the crash above.
This was reproduced with a test in `incremental_hydration_spec.ts`
that combines an immediately hydrated `@defer` block with an unrelated
`PendingTasks`-guarded `ViewContainerRef.createComponent()` whose
creation is delayed until after the defer block hydrates. The test
fails with the original code and passes with this fix.
The fix waits for `ApplicationRef.whenStable()` before running the
cleanup, but only when there are other pending tasks. This keeps the
usual case synchronous while making sure cleanup doesn't run while
other hydration work is still in progress.
The wait is intentionally conditional. An unconditional `whenStable()`
introduces another subscription to the shared stability signal.
Because this subscription is created after this function removes its
own pending task, callers that are already waiting on `whenStable()`
can resume one microtask before this cleanup does. That changes the
existing timing and breaks the `immediate` and `should create an
IntersectionObserver...` tests. Checking `hasPendingTasks` first
avoids creating that extra subscription when there is nothing else to
wait for, preserving the existing behavior.
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.

This fixes a production crash seen on pages that dynamically create non-defer components:
The crash was traced to
triggerHydrationForBlockQueue(). When a@deferblock finishes hydrating, it removes its ownPendingTasksentry and immediately callscleanupHydratedDeferBlocks()->cleanupDehydratedViews(). That cleanup walks all LViews registered withApplicationRefand removes dehydrated views that aren't associated with a defer block. This isn't safe while other async hydration work is still pending.For example, a dynamically created component can have its own
PendingTasksentry while itsViewContainerRef.createComponent()call is still waiting to run. If an unrelated defer block finishes first, its cleanup can remove that component's dehydrated view before the component gets a chance to claim it. The later creation then sees stale hydration state and can fail with the crash above.This was reproduced with a test in
incremental_hydration_spec.tsthat combines an immediately hydrated@deferblock with an unrelatedPendingTasks-guardedViewContainerRef.createComponent()whose creation is delayed until after the defer block hydrates. The test fails with the original code and passes with this fix.The fix waits for
ApplicationRef.whenStable()before running the cleanup, but only when there are other pending tasks. This keeps the usual case synchronous while making sure cleanup doesn't run while other hydration work is still in progress.The wait is intentionally conditional. An unconditional
whenStable()introduces another subscription to the shared stability signal. Because this subscription is created after this function removes its own pending task, callers that are already waiting onwhenStable()can resume one microtask before this cleanup does. That changes the existing timing and breaks theimmediateandshould create an IntersectionObserver...tests. CheckinghasPendingTasksfirst avoids creating that extra subscription when there is nothing else to wait for, preserving the existing behavior.