From bf6df6f18658dd0d477271f7eb969317ce1df024 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Tue, 18 Jun 2024 18:05:03 -0700 Subject: [PATCH] fix(core): do not activate event replay when no events are registered (#56509) This commit adds extra checks to handle a situation when an application has no events configured, but the Event Replay feature was enabled. This situation can happen when some routes in an application are mostly static, when other routes are more interactive. Resolves #56423. PR Close #56509 --- packages/core/src/hydration/event_replay.ts | 48 ++++++++++++++----- .../platform-server/test/event_replay_spec.ts | 48 ++++++++++++++++++- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/packages/core/src/hydration/event_replay.ts b/packages/core/src/hydration/event_replay.ts index e611742c783..7512d88664c 100644 --- a/packages/core/src/hydration/event_replay.ts +++ b/packages/core/src/hydration/event_replay.ts @@ -26,7 +26,11 @@ import {CLEANUP, LView, TView} from '../render3/interfaces/view'; import {isPlatformBrowser} from '../render3/util/misc_utils'; import {unwrapRNode} from '../render3/util/view_utils'; -import {IS_EVENT_REPLAY_ENABLED, IS_GLOBAL_EVENT_DELEGATION_ENABLED} from './tokens'; +import { + EVENT_REPLAY_ENABLED_DEFAULT, + IS_EVENT_REPLAY_ENABLED, + IS_GLOBAL_EVENT_DELEGATION_ENABLED, +} from './tokens'; import { GlobalEventDelegation, sharedStashFunction, @@ -50,6 +54,16 @@ function isGlobalEventDelegationEnabled(injector: Injector) { return injector.get(IS_GLOBAL_EVENT_DELEGATION_ENABLED, false); } +/** + * Determines whether Event Replay feature should be activated on the client. + */ +function shouldEnableEventReplay(injector: Injector) { + return ( + injector.get(IS_EVENT_REPLAY_ENABLED, EVENT_REPLAY_ENABLED_DEFAULT) && + !isGlobalEventDelegationEnabled(injector) + ); +} + /** * Returns a set of providers required to setup support for event replay. * Requires hydration to be enabled separately. @@ -58,19 +72,28 @@ export function withEventReplay(): Provider[] { return [ { provide: IS_EVENT_REPLAY_ENABLED, - useValue: true, + useFactory: () => { + let isEnabled = true; + if (isPlatformBrowser()) { + // Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature + // is enabled, but there are no events configured in this application, in which case + // we don't activate this feature, since there are no events to replay. + const appId = inject(APP_ID); + isEnabled = !!globalThis[CONTRACT_PROPERTY]?.[appId]; + } + return isEnabled; + }, }, { provide: ENVIRONMENT_INITIALIZER, useValue: () => { const injector = inject(Injector); - if (isGlobalEventDelegationEnabled(injector)) { - return; + if (isPlatformBrowser(injector) && shouldEnableEventReplay(injector)) { + setStashFn((rEl: RElement, eventName: string, listenerFn: VoidFunction) => { + sharedStashFunction(rEl, eventName, listenerFn); + jsactionSet.add(rEl as unknown as Element); + }); } - setStashFn((rEl: RElement, eventName: string, listenerFn: VoidFunction) => { - sharedStashFunction(rEl, eventName, listenerFn); - jsactionSet.add(rEl as unknown as Element); - }); }, multi: true, }, @@ -81,13 +104,14 @@ export function withEventReplay(): Provider[] { const injector = inject(Injector); const appRef = inject(ApplicationRef); return () => { + if (!shouldEnableEventReplay(injector)) { + return; + } + // Kick off event replay logic once hydration for the initial part // of the application is completed. This timing is similar to the unclaimed // dehydrated views cleanup timing. whenStable(appRef).then(() => { - if (isGlobalEventDelegationEnabled(injector)) { - return; - } const globalEventDelegation = injector.get(GlobalEventDelegation); initEventReplay(globalEventDelegation, injector); jsactionSet.forEach(removeListeners); @@ -112,8 +136,6 @@ function getJsactionData(container: EarlyJsactionDataContainer) { const initEventReplay = (eventDelegation: GlobalEventDelegation, injector: Injector) => { const appId = injector.get(APP_ID); // This is set in packages/platform-server/src/utils.ts - // Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature - // is enabled, but there are no events configured in an application. const container = globalThis[CONTRACT_PROPERTY]?.[appId]; const earlyJsactionData = getJsactionData(container)!; const eventContract = (eventDelegation.eventContract = new EventContract( diff --git a/packages/platform-server/test/event_replay_spec.ts b/packages/platform-server/test/event_replay_spec.ts index b0d7216539a..5f0e7d2e54c 100644 --- a/packages/platform-server/test/event_replay_spec.ts +++ b/packages/platform-server/test/event_replay_spec.ts @@ -7,7 +7,14 @@ */ import {DOCUMENT} from '@angular/common'; -import {Component, destroyPlatform, getPlatform, Type} from '@angular/core'; +import { + Component, + destroyPlatform, + ErrorHandler, + getPlatform, + PLATFORM_ID, + Type, +} from '@angular/core'; import {TestBed} from '@angular/core/testing'; import { withEventReplay, @@ -19,7 +26,14 @@ import {provideServerRendering} from '../public_api'; import {EVENT_DISPATCH_SCRIPT_ID, renderApplication} from '../src/utils'; import {EventPhase} from '@angular/core/primitives/event-dispatch'; -import {getAppContents, hydrate, render as renderHtml, resetTViewsFor} from './dom_utils'; +import { + getAppContents, + hydrate, + renderAndHydrate, + render as renderHtml, + resetTViewsFor, +} from './dom_utils'; +import {CONTRACT_PROPERTY} from '@angular/core/src/hydration/event_replay'; /** * Represents the