Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 544b9ee

Browse files
arturovtalxhub
authored andcommitted
fix(core): check whether application is destroyed before printing hydration stats (#59716)
In this commit, we check whether the application is destroyed before printing hydration stats. The application may be destroyed before it becomes stable, so when the `whenStableWithTimeout` resolves, the injector might already be in a destroyed state. As a result, calling `injector.get` would throw an error indicating that the injector has already been destroyed. PR Close #59716
1 parent 68dbaf5 commit 544b9ee

File tree

2 files changed

+76
-7
lines changed

2 files changed

+76
-7
lines changed

‎packages/core/src/hydration/api.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ function printHydrationStats(injector: Injector) {
156156
/**
157157
* Returns a Promise that is resolved when an application becomes stable.
158158
*/
159-
function whenStableWithTimeout(appRef: ApplicationRef, injector: Injector): Promise<void> {
159+
function whenStableWithTimeout(appRef: ApplicationRef): Promise<void> {
160160
const whenStablePromise = appRef.whenStable();
161161
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
162162
const timeoutTime = APPLICATION_IS_STABLE_TIMEOUT;
163-
const console = injector.get(Console);
164-
const ngZone = injector.get(NgZone);
163+
const console = appRef.injector.get(Console);
164+
const ngZone = appRef.injector.get(NgZone);
165165

166166
// The following call should not and does not prevent the app to become stable
167167
// We cannot use RxJS timer here because the app would remain unstable.
@@ -274,7 +274,7 @@ export function withDomHydration(): EnvironmentProviders {
274274
useFactory: () => {
275275
if (inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
276276
const appRef = inject(ApplicationRef);
277-
const injector = inject(Injector);
277+
278278
return () => {
279279
// Wait until an app becomes stable and cleanup all views that
280280
// were not claimed during the application bootstrap process.
@@ -283,11 +283,21 @@ export function withDomHydration(): EnvironmentProviders {
283283
//
284284
// Note: the cleanup task *MUST* be scheduled within the Angular zone in Zone apps
285285
// to ensure that change detection is properly run afterward.
286-
whenStableWithTimeout(appRef, injector).then(() => {
286+
whenStableWithTimeout(appRef).then(() => {
287+
// Note: we have to check whether the application is destroyed before
288+
// performing other operations with the `injector`.
289+
// The application may be destroyed **before** it becomes stable, so when
290+
// the `whenStableWithTimeout` resolves, the injector might already be in
291+
// a destroyed state. Thus, calling `injector.get` would throw an error
292+
// indicating that the injector has already been destroyed.
293+
if (appRef.destroyed) {
294+
return;
295+
}
296+
287297
cleanupDehydratedViews(appRef);
288298
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
289-
countBlocksSkippedByHydration(injector);
290-
printHydrationStats(injector);
299+
countBlocksSkippedByHydration(appRef.injector);
300+
printHydrationStats(appRef.injector);
291301
}
292302
});
293303
};

‎packages/platform-server/test/full_app_hydration_spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import '@angular/localize/init';
1111
import {
1212
CommonModule,
1313
DOCUMENT,
14+
isPlatformBrowser,
1415
isPlatformServer,
1516
NgComponentOutlet,
1617
NgFor,
@@ -35,6 +36,7 @@ import {
3536
inject,
3637
Input,
3738
NgZone,
39+
PendingTasks,
3840
Pipe,
3941
PipeTransform,
4042
PLATFORM_ID,
@@ -7023,6 +7025,63 @@ describe('platform-server full application hydration integration', () => {
70237025
expect(clientRootNode.textContent).toContain('Hi!');
70247026
},
70257027
);
7028+
7029+
it('should not throw an error when app is destroyed before becoming stable', async () => {
7030+
// Spy manually, because we may not be able to retrieve the `DebugConsole`
7031+
// after we destroy the application, but we still want to ensure that
7032+
// no error is thrown in the console.
7033+
const errorSpy = spyOn(console, 'error').and.callThrough();
7034+
const logs: string[] = [];
7035+
7036+
@Component({
7037+
standalone: true,
7038+
selector: 'app',
7039+
template: `Hi!`,
7040+
})
7041+
class SimpleComponent {
7042+
constructor() {
7043+
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
7044+
7045+
if (isBrowser) {
7046+
const pendingTasks = inject(PendingTasks);
7047+
// Given that, in a real-world scenario, some APIs add a pending
7048+
// task and don't remove it until the app is destroyed.
7049+
// This could be an HTTP request that contributes to app stability
7050+
// and does not respond until the app is destroyed.
7051+
pendingTasks.add();
7052+
}
7053+
}
7054+
}
7055+
7056+
const html = await ssr(SimpleComponent);
7057+
7058+
resetTViewsFor(SimpleComponent);
7059+
7060+
const appRef = await prepareEnvironmentAndHydrate(doc, html, SimpleComponent);
7061+
7062+
appRef.isStable.subscribe((isStable) => {
7063+
logs.push(`isStable=${isStable}`);
7064+
});
7065+
7066+
// Destroy the application before it becomes stable, because we added
7067+
// a task and didn't remove it explicitly.
7068+
appRef.destroy();
7069+
7070+
expect(logs).toEqual([
7071+
'isStable=false',
7072+
'isStable=true',
7073+
'isStable=false',
7074+
// In the end, the application became stable while being destroyed.
7075+
'isStable=true',
7076+
]);
7077+
7078+
// Wait for a microtask so that `whenStableWithTimeout` resolves.
7079+
await Promise.resolve();
7080+
7081+
// Ensure no error has been logged in the console,
7082+
// such as "injector has already been destroyed."
7083+
expect(errorSpy).not.toHaveBeenCalled();
7084+
});
70267085
});
70277086

70287087
describe('@if', () => {

0 commit comments

Comments
 (0)