mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(core): properly handle app stabilization with defer blocks (#61056)
Previously, the app was marked as stable prematurely. For more details, see https://github.com/angular/angular/issues/61038#issuecomment-2837917180
Closes: #61038
(cherry picked from commit de649c9cfd)
PR Close #61056
This commit is contained in:
parent
90b3355831
commit
400dbc5b89
10 changed files with 32 additions and 48 deletions
|
|
@ -1,8 +1,7 @@
|
|||
import {browser, by, element} from 'protractor';
|
||||
import {bootstrapClientApp, navigateTo, verifyNoBrowserErrors} from './util';
|
||||
|
||||
// TODO: this does not work with zoneless
|
||||
xdescribe('Defer E2E Tests', () => {
|
||||
describe('Defer E2E Tests', () => {
|
||||
beforeEach(async () => {
|
||||
// Don't wait for Angular since it is not bootstrapped automatically.
|
||||
await browser.waitForAngularEnabled(false);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {browser, by, element} from 'protractor';
|
|||
import {bootstrapClientApp, navigateTo, verifyNoBrowserErrors} from './util';
|
||||
|
||||
// TODO: this does not work with zoneless
|
||||
xdescribe('Http TransferState Lazy On Init', () => {
|
||||
describe('Http TransferState Lazy On Init', () => {
|
||||
beforeEach(async () => {
|
||||
// Don't wait for Angular since it is not bootstrapped automatically.
|
||||
await browser.waitForAngularEnabled(false);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {browser, by, element} from 'protractor';
|
|||
import {bootstrapClientApp, navigateTo, verifyNoBrowserErrors} from './util';
|
||||
|
||||
// TODO: this does not work with zoneless
|
||||
xdescribe('Http TransferState Lazy', () => {
|
||||
describe('Http TransferState Lazy', () => {
|
||||
beforeEach(async () => {
|
||||
// Don't wait for Angular since it is not bootstrapped automatically.
|
||||
await browser.waitForAngularEnabled(false);
|
||||
|
|
|
|||
|
|
@ -17,28 +17,10 @@ const browserDistFolder = resolve(serverDistFolder, '../browser');
|
|||
const indexHtml = readFileSync(join(browserDistFolder, 'index.csr.html'), 'utf-8');
|
||||
|
||||
async function runTest() {
|
||||
// Test and validate the errors are printed in the console.
|
||||
const originalConsoleError = console.error;
|
||||
const errors: string[] = [];
|
||||
console.error = (error, data) => errors.push(error.toString() + ' ' + data.toString());
|
||||
|
||||
try {
|
||||
await renderApplication(bootstrap, {
|
||||
document: indexHtml,
|
||||
url: '/error',
|
||||
});
|
||||
} catch {}
|
||||
|
||||
console.error = originalConsoleError;
|
||||
|
||||
// Test case
|
||||
if (!errors.some((e) => e.includes('Error in resolver'))) {
|
||||
errors.forEach(console.error);
|
||||
console.error(
|
||||
'\nError: expected rendering errors ("Error in resolver") to be printed in the console.\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await renderApplication(bootstrap, {
|
||||
document: indexHtml,
|
||||
url: '/error',
|
||||
});
|
||||
}
|
||||
|
||||
runTest();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {ApplicationConfig, provideZonelessChangeDetection} from '@angular/core';
|
||||
import {ApplicationConfig, provideExperimentalZonelessChangeDetection} from '@angular/core';
|
||||
import {provideClientHydration, withIncrementalHydration} from '@angular/platform-browser';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ import {routes} from './app.routes';
|
|||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZonelessChangeDetection(),
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideRouter(routes),
|
||||
provideClientHydration(withIncrementalHydration()),
|
||||
provideHttpClient(),
|
||||
|
|
|
|||
|
|
@ -29,11 +29,6 @@ export const routes: Routes = [
|
|||
{
|
||||
path: 'error',
|
||||
component: HelloWorldComponent,
|
||||
resolve: {
|
||||
'id': () => {
|
||||
throw new Error('Error in resolver.');
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'defer',
|
||||
|
|
|
|||
|
|
@ -7,23 +7,25 @@
|
|||
*/
|
||||
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'transfer-state-http',
|
||||
standalone: true,
|
||||
template: ` <div class="one">{{ responseOne }}</div> `,
|
||||
providers: [HttpClient],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TransferStateOnInitComponent implements OnInit {
|
||||
responseOne: string = '';
|
||||
|
||||
constructor(private readonly httpClient: HttpClient) {}
|
||||
private readonly httpClient: HttpClient = inject(HttpClient);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
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;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'transfer-state-http',
|
||||
|
|
@ -17,15 +17,19 @@ import {Component, OnInit} from '@angular/core';
|
|||
<div class="two">{{ responseTwo }}</div>
|
||||
`,
|
||||
providers: [HttpClient],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class TransferStateComponent implements OnInit {
|
||||
responseOne: string = '';
|
||||
responseTwo: string = '';
|
||||
private readonly httpClient: HttpClient = inject(HttpClient);
|
||||
private readonly cdr: ChangeDetectorRef = inject(ChangeDetectorRef);
|
||||
|
||||
constructor(private readonly httpClient: HttpClient) {
|
||||
constructor() {
|
||||
// Test that HTTP cache works when HTTP call is made in the constructor.
|
||||
this.httpClient.get<any>('http://localhost:4206/api').subscribe((response) => {
|
||||
this.responseOne = response.data;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -33,6 +37,7 @@ export class TransferStateComponent implements OnInit {
|
|||
// Test that HTTP cache works when HTTP call is made in a lifecycle hook.
|
||||
this.httpClient.get<any>('/api-2').subscribe((response) => {
|
||||
this.responseTwo = response.data;
|
||||
this.cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import {
|
|||
getParentBlockHydrationQueue,
|
||||
isIncrementalHydrationEnabled,
|
||||
} from '../hydration/utils';
|
||||
import {PendingTasksInternal} from '../pending_tasks';
|
||||
import {PendingTasks, PendingTasksInternal} from '../pending_tasks';
|
||||
import {assertLContainer} from '../render3/assert';
|
||||
import {getComponentDef, getDirectiveDef, getPipeDef} from '../render3/def_getters';
|
||||
import {getTemplateLocationDetails} from '../render3/instructions/element_validation';
|
||||
|
|
@ -205,8 +205,7 @@ export function triggerResourceLoading(
|
|||
}
|
||||
|
||||
// Indicate that an application is not stable and has a pending task.
|
||||
const pendingTasks = injector.get(PendingTasksInternal);
|
||||
const taskId = pendingTasks.add();
|
||||
const removeTask = injector.get(PendingTasks).add();
|
||||
|
||||
// The `dependenciesFn` might be `null` when all dependencies within
|
||||
// a given defer block were eagerly referenced elsewhere in a file,
|
||||
|
|
@ -215,7 +214,7 @@ export function triggerResourceLoading(
|
|||
tDetails.loadingPromise = Promise.resolve().then(() => {
|
||||
tDetails.loadingPromise = null;
|
||||
tDetails.loadingState = DeferDependenciesLoadingState.COMPLETE;
|
||||
pendingTasks.remove(taskId);
|
||||
removeTask();
|
||||
});
|
||||
return tDetails.loadingPromise;
|
||||
}
|
||||
|
|
@ -244,11 +243,6 @@ export function triggerResourceLoading(
|
|||
}
|
||||
}
|
||||
|
||||
// Loading is completed, we no longer need the loading Promise
|
||||
// and a pending task should also be removed.
|
||||
tDetails.loadingPromise = null;
|
||||
pendingTasks.remove(taskId);
|
||||
|
||||
if (failed) {
|
||||
tDetails.loadingState = DeferDependenciesLoadingState.FAILED;
|
||||
|
||||
|
|
@ -288,7 +282,13 @@ export function triggerResourceLoading(
|
|||
}
|
||||
}
|
||||
});
|
||||
return tDetails.loadingPromise;
|
||||
|
||||
return tDetails.loadingPromise.finally(() => {
|
||||
// Loading is completed, we no longer need the loading Promise
|
||||
// and a pending task should also be removed.
|
||||
tDetails.loadingPromise = null;
|
||||
removeTask();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -137,6 +137,7 @@
|
|||
"PREFETCH_TRIGGER_CLEANUP_FNS",
|
||||
"PRESERVE_HOST_CONTENT",
|
||||
"PRESERVE_HOST_CONTENT_DEFAULT",
|
||||
"PendingTasks",
|
||||
"PendingTasksInternal",
|
||||
"R3Injector",
|
||||
"REACTIVE_LVIEW_CONSUMER_NODE",
|
||||
|
|
|
|||
Loading…
Reference in a new issue