mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
fix(core): check whether application is destroyed before initializing event replay (#59789)
In this commit, we check whether the application is destroyed before initializing event replay. The application may be destroyed before it becomes stable, so when the `whenStable` 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 #59789
This commit is contained in:
parent
0f3fa5a2af
commit
79ae35577e
2 changed files with 81 additions and 3 deletions
|
|
@ -98,8 +98,8 @@ export function withEventReplay(): Provider[] {
|
|||
{
|
||||
provide: ENVIRONMENT_INITIALIZER,
|
||||
useValue: () => {
|
||||
const injector = inject(Injector);
|
||||
const appRef = injector.get(ApplicationRef);
|
||||
const appRef = inject(ApplicationRef);
|
||||
const {injector} = appRef;
|
||||
// We have to check for the appRef here due to the possibility of multiple apps
|
||||
// being present on the same page. We only want to enable event replay for the
|
||||
// apps that actually want it.
|
||||
|
|
@ -123,8 +123,8 @@ export function withEventReplay(): Provider[] {
|
|||
provide: APP_BOOTSTRAP_LISTENER,
|
||||
useFactory: () => {
|
||||
const appId = inject(APP_ID);
|
||||
const injector = inject(Injector);
|
||||
const appRef = inject(ApplicationRef);
|
||||
const {injector} = appRef;
|
||||
|
||||
return () => {
|
||||
// We have to check for the appRef here due to the possibility of multiple apps
|
||||
|
|
@ -155,6 +155,16 @@ export function withEventReplay(): Provider[] {
|
|||
// of the application is completed. This timing is similar to the unclaimed
|
||||
// dehydrated views cleanup timing.
|
||||
appRef.whenStable().then(() => {
|
||||
// Note: we have to check whether the application is destroyed before
|
||||
// performing other operations with the `injector`.
|
||||
// The application may be destroyed **before** it becomes stable, so when
|
||||
// the `whenStable` resolves, the injector might already be in
|
||||
// a destroyed state. Thus, calling `injector.get` would throw an error
|
||||
// indicating that the injector has already been destroyed.
|
||||
if (appRef.destroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventContractDetails = injector.get(JSACTION_EVENT_CONTRACT);
|
||||
initEventReplay(eventContractDetails, injector);
|
||||
const jsActionMap = injector.get(JSACTION_BLOCK_ELEMENT_MAP);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,11 @@ import {
|
|||
Directive,
|
||||
ErrorHandler,
|
||||
HostListener,
|
||||
inject,
|
||||
PendingTasks,
|
||||
PLATFORM_ID,
|
||||
} from '@angular/core';
|
||||
import {isPlatformBrowser} from '@angular/common';
|
||||
import {withEventReplay} from '@angular/platform-browser';
|
||||
|
||||
import {EventPhase} from '@angular/core/primitives/event-dispatch';
|
||||
|
|
@ -363,6 +366,71 @@ describe('event replay', () => {
|
|||
expect(getAppContents(html)).toContain('<script nonce="{{nonce}}">window.__jsaction_bootstrap');
|
||||
});
|
||||
|
||||
it('should not throw an error when app is destroyed before becoming stable', async () => {
|
||||
// Spy manually, because we may not be able to retrieve the `Console`
|
||||
// after we destroy the application, but we still want to ensure that
|
||||
// no error is thrown in the console.
|
||||
const errorSpy = spyOn(console, 'error').and.callThrough();
|
||||
const logs: string[] = [];
|
||||
|
||||
@Component({
|
||||
selector: 'app',
|
||||
standalone: true,
|
||||
template: `
|
||||
<button id="btn" (click)="onClick()"></button>
|
||||
`,
|
||||
})
|
||||
class AppComponent {
|
||||
constructor() {
|
||||
const isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
||||
if (isBrowser) {
|
||||
const pendingTasks = inject(PendingTasks);
|
||||
// Given that, in a real-world scenario, some APIs add a pending
|
||||
// task and don't remove it until the app is destroyed.
|
||||
// This could be an HTTP request that contributes to app stability
|
||||
// and does not respond until the app is destroyed.
|
||||
pendingTasks.add();
|
||||
}
|
||||
}
|
||||
|
||||
onClick(): void {}
|
||||
}
|
||||
const html = await ssr(AppComponent);
|
||||
const ssrContents = getAppContents(html);
|
||||
const doc = getDocument();
|
||||
|
||||
prepareEnvironment(doc, ssrContents);
|
||||
resetTViewsFor(AppComponent);
|
||||
const btn = doc.getElementById('btn')!;
|
||||
btn.click();
|
||||
const appRef = await hydrate(doc, AppComponent, {
|
||||
hydrationFeatures: () => [withEventReplay()],
|
||||
});
|
||||
|
||||
appRef.isStable.subscribe((isStable) => {
|
||||
logs.push(`isStable=${isStable}`);
|
||||
});
|
||||
|
||||
// Destroy the application before it becomes stable, because we added
|
||||
// a task and didn't remove it explicitly.
|
||||
appRef.destroy();
|
||||
|
||||
// Wait for a microtask so that `whenStable` resolves.
|
||||
await Promise.resolve();
|
||||
|
||||
expect(logs).toEqual([
|
||||
'isStable=false',
|
||||
'isStable=true',
|
||||
'isStable=false',
|
||||
// In the end, the application became stable while being destroyed.
|
||||
'isStable=true',
|
||||
]);
|
||||
|
||||
// Ensure no error has been logged in the console,
|
||||
// such as "injector has already been destroyed."
|
||||
expect(errorSpy).not.toHaveBeenCalledWith(/Injector has already been destroyed/);
|
||||
});
|
||||
|
||||
describe('bubbling behavior', () => {
|
||||
it('should propagate events', async () => {
|
||||
const onClickSpy = jasmine.createSpy();
|
||||
|
|
|
|||
Loading…
Reference in a new issue