fix(core): wait for HTTP in ngOnInit correctly before server render (#50573)

Previously, with `mergeMap` we did not cancel previous subscriptions to zoneIsStable which caused the application to be stablized before hand.

Closes: #50562

PR Close #50573
This commit is contained in:
Alan Agius
2023-06-05 11:25:05 +00:00
committed by Alex Rickabaugh
parent 5cfb6cacdf
commit c992109d6c
17 changed files with 315 additions and 6 deletions
@@ -2,7 +2,7 @@
"cli-hello-world": {
"uncompressed": {
"runtime": 908,
"main": 129295,
"main": 134468,
"polyfills": 33792
}
},
@@ -24,14 +24,14 @@
"forms": {
"uncompressed": {
"runtime": 888,
"main": 160778,
"main": 166256,
"polyfills": 33772
}
},
"animations": {
"uncompressed": {
"runtime": 898,
"main": 159461,
"main": 164757,
"polyfills": 33782
}
},
@@ -56,4 +56,4 @@
"polyfills": 33802
}
}
}
}
@@ -0,0 +1,44 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {browser, by, element} from 'protractor';
import {bootstrapClientApp, navigateTo, verifyNoBrowserErrors} from './util';
describe('Http TransferState Lazy On Init', () => {
beforeEach(async () => {
// Don't wait for Angular since it is not bootstrapped automatically.
await browser.waitForAngularEnabled(false);
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await navigateTo('http-transferstate-lazy-on-init');
});
afterEach(async () => {
// Make sure there were no client side errors.
await verifyNoBrowserErrors();
});
it('should transfer http state in lazy component', async () => {
// Test the contents from the server.
expect(await element(by.css('div.one')).getText()).toBe('API 1 response');
// Bootstrap the client side app and retest the contents
await bootstrapClientApp();
expect(await element(by.css('div.one')).getText()).toBe('API 1 response');
// Validate that there were no HTTP calls to '/api'.
const requests = await browser.executeScript(() => {
return performance.getEntriesByType('resource');
});
const apiRequests = (requests as {name: string}[])
.filter(({name}) => name.includes('/api'))
.map(({name}) => name);
expect(apiRequests).toEqual([]);
});
});
@@ -19,6 +19,13 @@ const routes: Routes = [
(m) => m.HttpTransferStateModule
),
},
{
path: 'http-transferstate-lazy-on-init',
loadChildren: () =>
import('./http-transferstate-lazy-on-init/http-transferstate-lazy-on-init.module').then(
(m) => m.HttpTransferStateOnInitModule
),
},
];
@NgModule({
@@ -0,0 +1,27 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {HttpClient} from '@angular/common/http';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'transfer-state-http-on-init',
template: ` <div class="one">{{ responseOne }}</div> `,
})
export class TransferStateComponentOnInit implements OnInit {
responseOne: string = '';
constructor(private readonly httpClient: HttpClient) {}
ngOnInit(): void {
// Test that HTTP cache works when HTTP call is made in a lifecycle hook.
this.httpClient.get<any>('http://localhost:4206/api').subscribe((response) => {
this.responseOne = response.data;
});
}
}
@@ -0,0 +1,18 @@
import {CommonModule} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {TransferStateComponentOnInit} from './http-transferstate-lazy-on-init.component';
const routes: Routes = [
{
path: '',
component: TransferStateComponentOnInit,
},
];
@NgModule({
imports: [RouterModule.forChild(routes), HttpClientModule, CommonModule],
declarations: [TransferStateComponentOnInit],
})
export class HttpTransferStateOnInitModule {}
@@ -18,4 +18,11 @@ export const routes: Routes = [
(c) => c.TransferStateComponent
),
},
{
path: 'http-transferstate-lazy-on-init',
loadComponent: () =>
import('./http-transferstate-lazy-on-init/http-transfer-state-on-init.component').then(
(c) => c.TransferStateOnInitComponent
),
},
];
@@ -0,0 +1,29 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {HttpClient} from '@angular/common/http';
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'transfer-state-http',
standalone: true,
template: ` <div class="one">{{ responseOne }}</div> `,
providers: [HttpClient],
})
export class TransferStateOnInitComponent implements OnInit {
responseOne: string = '';
constructor(private readonly httpClient: HttpClient) {}
ngOnInit(): void {
// Test that HTTP cache works when HTTP call is made in a lifecycle hook.
this.httpClient.get<any>('http://localhost:4206/api').subscribe((response) => {
this.responseOne = response.data;
});
}
}
+2 -2
View File
@@ -9,7 +9,7 @@
import './util/ng_jit_mode';
import {Observable, of, Subscription} from 'rxjs';
import {distinctUntilChanged, mergeMap, share} from 'rxjs/operators';
import {distinctUntilChanged, share, switchMap} from 'rxjs/operators';
import {ApplicationInitStatus} from './application_init';
import {PLATFORM_INITIALIZER} from './application_tokens';
@@ -847,7 +847,7 @@ export class ApplicationRef {
public readonly isStable: Observable<boolean> =
inject(InitialRenderPendingTasks)
.hasPendingTasks.pipe(
mergeMap(hasPendingTasks => hasPendingTasks ? of(false) : this.zoneIsStable),
switchMap(hasPendingTasks => hasPendingTasks ? of(false) : this.zoneIsStable),
distinctUntilChanged(),
share(),
);
@@ -500,6 +500,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -851,6 +857,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -1034,6 +1043,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1175,6 +1187,9 @@
{
"name": "makeTimingAst"
},
{
"name": "map"
},
{
"name": "markAsComponentHost"
},
@@ -1187,6 +1202,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1415,6 +1433,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -542,6 +542,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -914,6 +920,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -1100,6 +1109,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1241,6 +1253,9 @@
{
"name": "makeTimingAst"
},
{
"name": "map"
},
{
"name": "markAsComponentHost"
},
@@ -1253,6 +1268,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1490,6 +1508,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -407,6 +407,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -689,6 +695,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -866,6 +875,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -980,6 +992,9 @@
{
"name": "makeRecord"
},
{
"name": "map"
},
{
"name": "markAsComponentHost"
},
@@ -992,6 +1007,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1181,6 +1199,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -548,6 +548,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -1199,6 +1205,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1379,6 +1388,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeErrors"
},
@@ -1646,6 +1658,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -536,6 +536,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -1163,6 +1169,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1337,6 +1346,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeErrors"
},
@@ -1622,6 +1634,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -314,6 +314,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -536,6 +542,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -686,6 +695,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -767,6 +779,9 @@
{
"name": "makeRecord"
},
{
"name": "map"
},
{
"name": "markViewDirty"
},
@@ -776,6 +791,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -929,6 +947,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -458,6 +458,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -746,6 +752,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -926,6 +935,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1046,6 +1058,9 @@
{
"name": "makeRecord"
},
{
"name": "map"
},
{
"name": "markViewDirty"
},
@@ -1055,6 +1070,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1241,6 +1259,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwIfEmpty"
},
@@ -374,6 +374,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -614,6 +620,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -770,6 +779,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -860,6 +872,9 @@
{
"name": "makeRecord"
},
{
"name": "map"
},
{
"name": "markViewDirty"
},
@@ -869,6 +884,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1031,6 +1049,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},
@@ -437,6 +437,12 @@
{
"name": "Subscription"
},
{
"name": "SwitchMapOperator"
},
{
"name": "SwitchMapSubscriber"
},
{
"name": "TESTABILITY"
},
@@ -812,6 +818,9 @@
{
"name": "forwardRef"
},
{
"name": "from"
},
{
"name": "fromArray"
},
@@ -1037,6 +1046,9 @@
{
"name": "injectableDefOrInjectorDefFactory"
},
{
"name": "innerSubscribe"
},
{
"name": "insertBloom"
},
@@ -1175,6 +1187,9 @@
{
"name": "makeRecord"
},
{
"name": "map"
},
{
"name": "markAsComponentHost"
},
@@ -1190,6 +1205,9 @@
{
"name": "maybeWrapInNotSelector"
},
{
"name": "mergeAll"
},
{
"name": "mergeHostAttribute"
},
@@ -1406,6 +1424,9 @@
{
"name": "subscribeToArray"
},
{
"name": "switchMap"
},
{
"name": "throwProviderNotFoundError"
},